From 3d98c62a242c445980b8e095904c7997c7006385 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 24 Jul 2024 13:22:19 +0200 Subject: [PATCH 1/8] DSC Configuration Migration Tool module --- powershell-helpers/README.md | 13 + powershell-helpers/dscCfgMigMod.psd1 | 47 +++ powershell-helpers/dscCfgMigMod.psm1 | 384 ++++++++++++++++++ .../tests/dscCfgMigMod.tests.ps1 | 24 ++ 4 files changed, 468 insertions(+) create mode 100644 powershell-helpers/README.md create mode 100644 powershell-helpers/dscCfgMigMod.psd1 create mode 100644 powershell-helpers/dscCfgMigMod.psm1 create mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md new file mode 100644 index 00000000..d48f4e87 --- /dev/null +++ b/powershell-helpers/README.md @@ -0,0 +1,13 @@ +# Introduction + +The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: + +- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). + +## Getting started + +To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: + +1. Open a PowerShell terminal session +2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` +3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 new file mode 100644 index 00000000..83d1ac09 --- /dev/null +++ b/powershell-helpers/dscCfgMigMod.psd1 @@ -0,0 +1,47 @@ +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'dscCfgMigMod.psm1' + + # Version number of this module. + moduleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' + + # Author of this module + Author = 'Microsoft Corporation' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell Desired State Configuration Migration Module helper' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('powershell-yaml') + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'ConvertTo-DscJson' + 'ConvertTo-DscYaml' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + PrivateData = @{ + PSData = @{ + ProjectUri = 'https://github.com/PowerShell/dsc' + } + } +} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 new file mode 100644 index 00000000..c61ff4bf --- /dev/null +++ b/powershell-helpers/dscCfgMigMod.psm1 @@ -0,0 +1,384 @@ +#region Main functions +function ConvertTo-DscJson +{ + <# + .SYNOPSIS + Convert a PowerShell DSC configuration document to DSC version 3 JSON format. + + .DESCRIPTION + The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. + + .PARAMETER Path + The path to valid PowerShell DSC configuration document + + .EXAMPLE + PS C:\> $configuration = @' + Configuration TestResource { + Import-DscResource -ModuleName TestResource + Node localhost { + TestResource 'Configure test resource' { + Ensure = 'Absent' + Name = 'MyTestResource' + } + } + } + '@ + PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' + PS C:\> $configuration | Out-File -FilePath $Path + PS C:\> ConvertTo-DscJson -Path $Path + + Returns: + { + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", + "resources": { + "name": "TestResource", + "type": "Microsoft.DSC/PowerShell", + "properties": { + "resources": [ + { + "name": "Configure test resource", + "type": "TestResource/TestResource", + "properties": { + "Name": "MyTestResource", + "Ensure": "Absent" + } + } + ] + } + } + } + + .NOTES + Tags: DSC, Migration, JSON + #> + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = BuildConfigurationDocument -Path $Path + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} + +function ConvertTo-DscYaml +{ + <# + .SYNOPSIS + Convert a PowerShell DSC configuration document to DSC version 3 YAML format. + + .DESCRIPTION + The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. + + .PARAMETER Path + The path to valid PowerShell DSC configuration document + + .EXAMPLE + PS C:\> $configuration = @' + Configuration TestResource { + Import-DscResource -ModuleName TestResource + Node localhost { + TestResource 'Configure test resource' { + Ensure = 'Absent' + Name = 'MyTestResource' + } + } + } + '@ + PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' + PS C:\> $configuration | Out-File -FilePath $Path + PS C:\> ConvertTo-DscYaml -Path $Path + + Returns: + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + name: TestResource + type: Microsoft.DSC/PowerShell + properties: + resources: + - name: Configure test resource + type: TestResource/TestResource + properties: + Name: MyTestResource + Ensure: Absent + + .NOTES + Tags: DSC, Migration, YAML + #> + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = BuildConfigurationDocument -Path $Path -Format YAML + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} +#endRegion Main functions + +#region Helper functions +function FindAndExtractConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + if (-not (TestPathExtension $Path)) + { + return @{} + } + + # Parse the abstract syntax tree to get all hash table values representing the configuration resources + [System.Management.Automation.Language.Token[]] $tokens = $null + [System.Management.Automation.Language.ParseError[]] $errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) + $configurations = $ast.FindAll({$args[0].GetType().Name -like 'HashtableAst'}, $true) + + # Create configuration document resource class (can be re-used) + $configurationDocument = [DscConfigurationResource]::new() + + # Build simple regex + $regex = [regex]::new('Configuration\s+(\w+)') + $configValue = $regex.Matches($ast.Extent.Text).Value + + if (-not $configValue) + { + return + } + + $documentConfigurationName = $configValue.TrimStart('Configuration').Trim(" ") + + # Start to build the outer basic format + $configurationDocument.name = $documentConfigurationName + $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type + + # Bag to hold resources + $resourceProps = [System.Collections.Generic.List[object]]::new() + + foreach ($configuration in $configurations) + { + # Get parent configuration details + $resourceName = ($configuration.Parent.CommandElements.Value | Select-Object -Last 1 ) + $resourceConfigurationName = ($configuration.Parent.CommandElements.Value | Select-Object -First 1) + + # Get module details + $module = Get-DscResource -Name $resourceConfigurationName -ErrorAction SilentlyContinue + + # Build the module + $resource = [DscConfigurationResource]::new() + $resource.properties = $configuration.SafeGetValue() + $resource.name = $resourceName + $resource.type = ("{0}/{1}" -f $module.ModuleName, $resourceConfigurationName) + # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource + # $document.ModuleName = $module.ModuleName + + Write-Verbose ("Adding document with data") + Write-Verbose ($resource | ConvertTo-Json | Out-String) + $resourceProps.Add($resource) + } + + # Add all the resources + $configurationDocument.properties = @{ + resources = $resourceProps + } + + return $configurationDocument +} + +function BuildConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [ValidateSet('JSON', 'YAML')] + [System.String] + $Format = 'JSON' + ) + + $configurationDocument = [ordered]@{ + "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder + resources = FindAndExtractConfigurationDocument -Path $Path + } + + switch ($Format) + { + "JSON" { + $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) + } + "YAML" { + if (TestYamlModule) + { + $inputObject = ($configurationDocument | ConvertTo-Yaml) + } + else + { + $inputObject = @{} + } + } + default { + $inputObject = $configurationDocument + } + } + + return $inputObject +} + +function TestPathExtension +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + $res = $true + + if (-not (Test-Path $Path)) + { + $res = $false + } + + if (([System.IO.Path]::GetExtension($Path) -ne ".ps1")) + { + $res = $false + } + + return $res +} + +function TestYamlModule +{ + if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) + { + return $false + } + + return $true +} + +function GetPowerShellPath +{ + param + ( + $Path + ) + + $knownPath = @( + "$env:USERPROFILE\Documents\PowerShell\Modules", + "$env:ProgramFiles\PowerShell\Modules", + "$env:ProgramFiles\PowerShell\7\Modules" + ) + + foreach ($known in $knownPath) + { + if ($Path.StartsWith($known)) + { + return $true + } + } + + return $false +} + +function GetWindowsPowerShellPath +{ + param + ( + $Path + ) + + $knownPath = @( + "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", + "$env:ProgramFiles\WindowsPowerShell\Modules", + "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" + ) + + foreach ($known in $knownPath) + { + if ($Path.StartsWith($known)) + { + return $true + } + } + + return $false +} + +function ResolvePowerShellPath +{ + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + if (-not (Test-Path $Path)) + { + return + } + + if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) + { + return + } + + if (GetPowerShellPath -Path $Path) + { + return "Microsoft.DSC/PowerShell" + } + + if (GetWindowsPowerShellPath -Path $Path) + { + return "Microsoft.Windows/WindowsPowerShell" + } + + return $null # TODO: Or default Microsoft.DSC/PowerShell +} + +#endRegion Helper functions + +#region Classes +class DscConfigurationResource +{ + [string] $name + [string] $type + [hashtable] $properties +} +#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 new file mode 100644 index 00000000..b966993a --- /dev/null +++ b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 @@ -0,0 +1,24 @@ +Describe "DSC Configuration Migration Module tests" { + BeforeAll { + $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path + $modLoad = Import-Module $modPath -Force -PassThru + } + + Context "ConvertTo-DscYaml" { + It "Should create an empty resource block" { + $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) + $res.resources | Should -BeNullOrEmpty + } + } + + Context "ConvertTo-DscJson" { + It "Should create an empty resource block" { + $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) + $res.resources | Should -BeNullOrEmpty + } + } + + AfterAll { + Remove-Module -Name $modLoad.Name -Force + } +} From 9443e1cdd619f85cdb29606093b449e65cd67c6e Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Thu, 25 Jul 2024 17:11:07 +0200 Subject: [PATCH 2/8] Add class-based operation methods in DSCResourceInfo --- .../psDscAdapter/psDscAdapter.psm1 | 270 +++++++++++++----- 1 file changed, 196 insertions(+), 74 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 07b3763e..f31f672d 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -22,25 +22,19 @@ function Import-PSDSCModule { $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -function Get-DSCResourceModules -{ +function Get-DSCResourceModules { $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() - foreach ($folder in $listPSModuleFolders) - { - if (!(Test-Path $folder)) - { + foreach ($folder in $listPSModuleFolders) { + if (!(Test-Path $folder)) { continue } - foreach($moduleFolder in Get-ChildItem $folder -Directory) - { + foreach ($moduleFolder in Get-ChildItem $folder -Directory) { $addModule = $false - foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) - { + foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if($null -ne $containsDSCResource) - { + if ($null -ne $containsDSCResource) { $dscModulePsd1List.Add($psd1) | Out-Null } } @@ -57,39 +51,32 @@ function Add-AstMembers { $Properties ) - foreach($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object {$_.Name -eq $TypeConstraint.TypeName.Name} + foreach ($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } if ($t) { Add-AstMembers $AllTypeDefinitions $t $Properties } } - foreach ($member in $TypeAst.Members) - { + foreach ($member in $TypeAst.Members) { $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($property -eq $null) -or ($property.IsStatic)) - { + if (($property -eq $null) -or ($property.IsStatic)) { continue; } $skipProperty = $true $isKeyProperty = $false - foreach($attr in $property.Attributes) - { - if ($attr.TypeName.Name -eq 'DscProperty') - { + foreach ($attr in $property.Attributes) { + if ($attr.TypeName.Name -eq 'DscProperty') { $skipProperty = $false - foreach($attrArg in $attr.NamedArguments) - { - if ($attrArg.ArgumentName -eq 'Key') - { + foreach ($attrArg in $attr.NamedArguments) { + if ($attrArg.ArgumentName -eq 'Key') { $isKeyProperty = $true break } } } } - if ($skipProperty) - { + if ($skipProperty) { continue; } @@ -101,8 +88,7 @@ function Add-AstMembers { } } -function FindAndParseResourceDefinitions -{ +function FindAndParseResourceDefinitions { [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -111,13 +97,11 @@ function FindAndParseResourceDefinitions [string]$moduleVersion ) - if (-not (Test-Path $filePath)) - { + if (-not (Test-Path $filePath)) { return } - if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) - { + if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) { return } @@ -126,8 +110,7 @@ function FindAndParseResourceDefinitions [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) - foreach($e in $errors) - { + foreach ($e in $errors) { $e | Out-String | Write-DscTrace -Operation Error } @@ -140,12 +123,9 @@ function FindAndParseResourceDefinitions $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - foreach($typeDefinitionAst in $typeDefinitions) - { - foreach($a in $typeDefinitionAst.Attributes) - { - if ($a.TypeName.Name -eq 'DscResource') - { + foreach ($typeDefinitionAst in $typeDefinitions) { + foreach ($a in $typeDefinitionAst.Attributes) { + if ($a.TypeName.Name -eq 'DscResource') { $DscResourceInfo = [DscResourceInfo]::new() $DscResourceInfo.Name = $typeDefinitionAst.Name $DscResourceInfo.ResourceType = $typeDefinitionAst.Name @@ -157,8 +137,10 @@ function FindAndParseResourceDefinitions $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) $DscResourceInfo.Version = $moduleVersion + $DscResourceInfo.Operations = GetResourceOperationMethods -resourceName $typeDefinitionAst.Name -filePath $filePath $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() + Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties $resourceList.Add($DscResourceInfo) @@ -169,8 +151,7 @@ function FindAndParseResourceDefinitions return $resourceList } -function LoadPowerShellClassResourcesFromModule -{ +function LoadPowerShellClassResourcesFromModule { [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -179,29 +160,24 @@ function LoadPowerShellClassResourcesFromModule "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace - if ($moduleInfo.RootModule) - { - if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and + if ($moduleInfo.RootModule) { + if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and ([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".ps1") -and - (-not $z.NestedModules)) - { + (-not $z.NestedModules)) { "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace return [System.Collections.Generic.List[DscResourceInfo]]::new() } $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule } - else - { + else { $scriptPath = $moduleInfo.Path; } $Resources = FindAndParseResourceDefinitions $scriptPath $moduleInfo.Version - if ($moduleInfo.NestedModules) - { - foreach ($nestedModule in $moduleInfo.NestedModules) - { + if ($moduleInfo.NestedModules) { + foreach ($nestedModule in $moduleInfo.NestedModules) { $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule if ($resourcesOfNestedModules) { $Resources.AddRange($resourcesOfNestedModules) @@ -212,6 +188,153 @@ function LoadPowerShellClassResourcesFromModule return $Resources } +function GetResourceOperationMethods { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $resourceName, + + [Parameter(Mandatory = $true)] + [string] $filePath + ) + + # dot source scope + try { + . (LoadClassAndEnumsFromModuleFile -filePath $filePath) + } catch { + ("Module: '{0}' not loaded for resource operation discovery."-f $filePath) | Write-DscTrace + } + + $inputObject = ReturnTypeNameObject -TypeName $resourceName + + if (-not $inputObject) { + return @( + 'Get', + 'Test', + 'Set' + ) + } + + # TODO: There might be more properties available + $knownMemberTypes = @('Equals', 'GetHashCode', 'GetType', 'ToString') + return ($inputObject | Get-Member | Where-Object { $_.MemberType -eq 'Method' -and $_.Name -notin $knownMemberTypes }).Name +} + +function ReturnTypeNameObject { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $TypeName + ) + + try { + $inputObject = New-Object -TypeName $TypeName -ErrorAction Stop + } + catch { + "Could not create: $TypeName" | Write-DscTrace + } + + return $inputObject +} + +function LoadClassAndEnumsFromModuleFile { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $filePath + ) + + if (-not (Test-Path $filePath -ErrorAction SilentlyContinue)) { + return + } + + $ctx = Get-Content $filePath + + $string = @( + 'using namespace System.Collections.Generic', # TODO: Figure away out to get using statements included + (GetEnumCodeBlock -Content $ctx), + (GetClassCodeBlock -Content $ctx) + ) + + # TODO: Might have to do something with the path + $outPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [System.Guid]::NewGuid().Guid) + $string | Out-File -FilePath $outPath + + return $outPath +} + +function GetClassCodeBlock { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content + ) + + $ctx = $Content + + $lines = ($ctx | Select-String -Pattern '\[DSCResource\(\)]').LineNumber + if ($lines.Count -eq 0 ) { + return + } + + $lastLineNumber = $lines[-1] + $index = 1 + # Bring all class strings together after the last one + $classStrings = foreach ($line in $lines) { + if ($line -eq $lastLineNumber) { + $lastModuleLine = $ctx.Length + + $line = $line - 1 + $block = $ctx[$line..$lastModuleLine] + $block + break + } + + $line = $line - 1 + $curlyBracketLine = FindCurlyBracket -Content $ctx -LineNumber $lines[$index] + $block = $ctx[$line..$curlyBracketLine] + + $index++ + $block + } + + return $classStrings +} + +function GetEnumCodeBlock { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content + ) + + # Build regex to catch enum blocks + $regex = [regex]::new('enum\s+(\w+)\s*\{([^}]+)\}') + + $hits = $regex.Matches($Content) + + # return as single lines + return ($hits.Value -Split " ") +} + +function FindCurlyBracket { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content, + + [Parameter(Mandatory = $true)] + [int] $LineNumber + ) + do { + if ($Content[$LineNumber] -eq "}") { + return $LineNumber + } + + $LineNumber-- + } while ($LineNumber -ne 0) +} + <# public function Invoke-DscCacheRefresh .SYNOPSIS This function caches the results of the Get-DscResource call to optimize performance. @@ -237,7 +360,8 @@ function Invoke-DscCacheRefresh { $cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } else { + } + else { # PS 6+ on Linux/Mac Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } @@ -249,8 +373,9 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '"+$cache.CacheSchemaVersion+"' (expected '"+$script:CurrentCacheSchemaVersion+"')" | Write-DscTrace - } else { + "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace + } + else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { @@ -259,8 +384,7 @@ function Invoke-DscCacheRefresh { "Filtered DscResourceCache cache is empty" | Write-DscTrace } - else - { + else { "Checking cache for stale entries" | Write-DscTrace foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -268,20 +392,19 @@ function Invoke-DscCacheRefresh { $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) - { + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) { "Detected stale cache entry '$($_.Name)'" | Write-DscTrace $refreshCache = $true break } } - if ($refreshCache) {break} + if ($refreshCache) { break } } "Checking cache for stale PSModulePath" | Write-DscTrace - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -309,11 +432,10 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules - if($null -ne $dscResourceModulePsd1s) { + if ($null -ne $dscResourceModulePsd1s) { $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) $processedModuleNames = @{} - foreach ($mod in $modules) - { + foreach ($mod in $modules) { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { $processedModuleNames.Add($mod.Name, $true) @@ -337,20 +459,20 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } $dscResourceCacheEntries += [dscResourceCacheEntry]@{ Type = "$moduleName/$($dscResource.Name)" DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes + LastWriteTimes = $lastWriteTimes } } [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion @@ -462,12 +584,12 @@ function Invoke-DscOperation { } 'Test' { $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} + $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } } 'Export' { $t = $dscResourceInstance.GetType() $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null,$null) + $resultArray = $method.Invoke($null, $null) $addToActualState = $resultArray } } @@ -534,8 +656,7 @@ enum dscResourceType { Composite } -class DscResourcePropertyInfo -{ +class DscResourcePropertyInfo { [string] $Name [string] $PropertyType [bool] $IsMandatory @@ -556,4 +677,5 @@ class DscResourceInfo { [string] $ImplementedAs [string] $CompanyName [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties + [System.String[]] $Operations } From 39c57b3067855607fe8cc67ef4c02d5f78163fc2 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Fri, 26 Jul 2024 05:31:49 +0200 Subject: [PATCH 3/8] Revert "Add class-based operation methods in DSCResourceInfo" This reverts commit 9443e1cdd619f85cdb29606093b449e65cd67c6e. --- .../psDscAdapter/psDscAdapter.psm1 | 270 +++++------------- 1 file changed, 74 insertions(+), 196 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index f31f672d..07b3763e 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -22,19 +22,25 @@ function Import-PSDSCModule { $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -function Get-DSCResourceModules { +function Get-DSCResourceModules +{ $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() - foreach ($folder in $listPSModuleFolders) { - if (!(Test-Path $folder)) { + foreach ($folder in $listPSModuleFolders) + { + if (!(Test-Path $folder)) + { continue } - foreach ($moduleFolder in Get-ChildItem $folder -Directory) { + foreach($moduleFolder in Get-ChildItem $folder -Directory) + { $addModule = $false - foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { + foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) + { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if ($null -ne $containsDSCResource) { + if($null -ne $containsDSCResource) + { $dscModulePsd1List.Add($psd1) | Out-Null } } @@ -51,32 +57,39 @@ function Add-AstMembers { $Properties ) - foreach ($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } + foreach($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object {$_.Name -eq $TypeConstraint.TypeName.Name} if ($t) { Add-AstMembers $AllTypeDefinitions $t $Properties } } - foreach ($member in $TypeAst.Members) { + foreach ($member in $TypeAst.Members) + { $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($property -eq $null) -or ($property.IsStatic)) { + if (($property -eq $null) -or ($property.IsStatic)) + { continue; } $skipProperty = $true $isKeyProperty = $false - foreach ($attr in $property.Attributes) { - if ($attr.TypeName.Name -eq 'DscProperty') { + foreach($attr in $property.Attributes) + { + if ($attr.TypeName.Name -eq 'DscProperty') + { $skipProperty = $false - foreach ($attrArg in $attr.NamedArguments) { - if ($attrArg.ArgumentName -eq 'Key') { + foreach($attrArg in $attr.NamedArguments) + { + if ($attrArg.ArgumentName -eq 'Key') + { $isKeyProperty = $true break } } } } - if ($skipProperty) { + if ($skipProperty) + { continue; } @@ -88,7 +101,8 @@ function Add-AstMembers { } } -function FindAndParseResourceDefinitions { +function FindAndParseResourceDefinitions +{ [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -97,11 +111,13 @@ function FindAndParseResourceDefinitions { [string]$moduleVersion ) - if (-not (Test-Path $filePath)) { + if (-not (Test-Path $filePath)) + { return } - if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) { + if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) + { return } @@ -110,7 +126,8 @@ function FindAndParseResourceDefinitions { [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) - foreach ($e in $errors) { + foreach($e in $errors) + { $e | Out-String | Write-DscTrace -Operation Error } @@ -123,9 +140,12 @@ function FindAndParseResourceDefinitions { $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - foreach ($typeDefinitionAst in $typeDefinitions) { - foreach ($a in $typeDefinitionAst.Attributes) { - if ($a.TypeName.Name -eq 'DscResource') { + foreach($typeDefinitionAst in $typeDefinitions) + { + foreach($a in $typeDefinitionAst.Attributes) + { + if ($a.TypeName.Name -eq 'DscResource') + { $DscResourceInfo = [DscResourceInfo]::new() $DscResourceInfo.Name = $typeDefinitionAst.Name $DscResourceInfo.ResourceType = $typeDefinitionAst.Name @@ -137,10 +157,8 @@ function FindAndParseResourceDefinitions { $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) $DscResourceInfo.Version = $moduleVersion - $DscResourceInfo.Operations = GetResourceOperationMethods -resourceName $typeDefinitionAst.Name -filePath $filePath $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() - Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties $resourceList.Add($DscResourceInfo) @@ -151,7 +169,8 @@ function FindAndParseResourceDefinitions { return $resourceList } -function LoadPowerShellClassResourcesFromModule { +function LoadPowerShellClassResourcesFromModule +{ [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -160,24 +179,29 @@ function LoadPowerShellClassResourcesFromModule { "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace - if ($moduleInfo.RootModule) { - if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and + if ($moduleInfo.RootModule) + { + if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and ([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".ps1") -and - (-not $z.NestedModules)) { + (-not $z.NestedModules)) + { "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace return [System.Collections.Generic.List[DscResourceInfo]]::new() } $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule } - else { + else + { $scriptPath = $moduleInfo.Path; } $Resources = FindAndParseResourceDefinitions $scriptPath $moduleInfo.Version - if ($moduleInfo.NestedModules) { - foreach ($nestedModule in $moduleInfo.NestedModules) { + if ($moduleInfo.NestedModules) + { + foreach ($nestedModule in $moduleInfo.NestedModules) + { $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule if ($resourcesOfNestedModules) { $Resources.AddRange($resourcesOfNestedModules) @@ -188,153 +212,6 @@ function LoadPowerShellClassResourcesFromModule { return $Resources } -function GetResourceOperationMethods { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $resourceName, - - [Parameter(Mandatory = $true)] - [string] $filePath - ) - - # dot source scope - try { - . (LoadClassAndEnumsFromModuleFile -filePath $filePath) - } catch { - ("Module: '{0}' not loaded for resource operation discovery."-f $filePath) | Write-DscTrace - } - - $inputObject = ReturnTypeNameObject -TypeName $resourceName - - if (-not $inputObject) { - return @( - 'Get', - 'Test', - 'Set' - ) - } - - # TODO: There might be more properties available - $knownMemberTypes = @('Equals', 'GetHashCode', 'GetType', 'ToString') - return ($inputObject | Get-Member | Where-Object { $_.MemberType -eq 'Method' -and $_.Name -notin $knownMemberTypes }).Name -} - -function ReturnTypeNameObject { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $TypeName - ) - - try { - $inputObject = New-Object -TypeName $TypeName -ErrorAction Stop - } - catch { - "Could not create: $TypeName" | Write-DscTrace - } - - return $inputObject -} - -function LoadClassAndEnumsFromModuleFile { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $filePath - ) - - if (-not (Test-Path $filePath -ErrorAction SilentlyContinue)) { - return - } - - $ctx = Get-Content $filePath - - $string = @( - 'using namespace System.Collections.Generic', # TODO: Figure away out to get using statements included - (GetEnumCodeBlock -Content $ctx), - (GetClassCodeBlock -Content $ctx) - ) - - # TODO: Might have to do something with the path - $outPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [System.Guid]::NewGuid().Guid) - $string | Out-File -FilePath $outPath - - return $outPath -} - -function GetClassCodeBlock { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content - ) - - $ctx = $Content - - $lines = ($ctx | Select-String -Pattern '\[DSCResource\(\)]').LineNumber - if ($lines.Count -eq 0 ) { - return - } - - $lastLineNumber = $lines[-1] - $index = 1 - # Bring all class strings together after the last one - $classStrings = foreach ($line in $lines) { - if ($line -eq $lastLineNumber) { - $lastModuleLine = $ctx.Length - - $line = $line - 1 - $block = $ctx[$line..$lastModuleLine] - $block - break - } - - $line = $line - 1 - $curlyBracketLine = FindCurlyBracket -Content $ctx -LineNumber $lines[$index] - $block = $ctx[$line..$curlyBracketLine] - - $index++ - $block - } - - return $classStrings -} - -function GetEnumCodeBlock { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content - ) - - # Build regex to catch enum blocks - $regex = [regex]::new('enum\s+(\w+)\s*\{([^}]+)\}') - - $hits = $regex.Matches($Content) - - # return as single lines - return ($hits.Value -Split " ") -} - -function FindCurlyBracket { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content, - - [Parameter(Mandatory = $true)] - [int] $LineNumber - ) - do { - if ($Content[$LineNumber] -eq "}") { - return $LineNumber - } - - $LineNumber-- - } while ($LineNumber -ne 0) -} - <# public function Invoke-DscCacheRefresh .SYNOPSIS This function caches the results of the Get-DscResource call to optimize performance. @@ -360,8 +237,7 @@ function Invoke-DscCacheRefresh { $cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } - else { + } else { # PS 6+ on Linux/Mac Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } @@ -373,9 +249,8 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace - } - else { + "Incompatible version of cache in file '"+$cache.CacheSchemaVersion+"' (expected '"+$script:CurrentCacheSchemaVersion+"')" | Write-DscTrace + } else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { @@ -384,7 +259,8 @@ function Invoke-DscCacheRefresh { "Filtered DscResourceCache cache is empty" | Write-DscTrace } - else { + else + { "Checking cache for stale entries" | Write-DscTrace foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -392,19 +268,20 @@ function Invoke-DscCacheRefresh { $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) { + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) + { "Detected stale cache entry '$($_.Name)'" | Write-DscTrace $refreshCache = $true break } } - if ($refreshCache) { break } + if ($refreshCache) {break} } "Checking cache for stale PSModulePath" | Write-DscTrace - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -432,10 +309,11 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules - if ($null -ne $dscResourceModulePsd1s) { + if($null -ne $dscResourceModulePsd1s) { $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) $processedModuleNames = @{} - foreach ($mod in $modules) { + foreach ($mod in $modules) + { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { $processedModuleNames.Add($mod.Name, $true) @@ -459,20 +337,20 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } $dscResourceCacheEntries += [dscResourceCacheEntry]@{ Type = "$moduleName/$($dscResource.Name)" DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes + LastWriteTimes = $lastWriteTimes } } [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion @@ -584,12 +462,12 @@ function Invoke-DscOperation { } 'Test' { $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } + $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} } 'Export' { $t = $dscResourceInstance.GetType() $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null, $null) + $resultArray = $method.Invoke($null,$null) $addToActualState = $resultArray } } @@ -656,7 +534,8 @@ enum dscResourceType { Composite } -class DscResourcePropertyInfo { +class DscResourcePropertyInfo +{ [string] $Name [string] $PropertyType [bool] $IsMandatory @@ -677,5 +556,4 @@ class DscResourceInfo { [string] $ImplementedAs [string] $CompanyName [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties - [System.String[]] $Operations } From 0df185a8b2e0aba87a4e87206936ba2934b21353 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 24 Oct 2024 10:04:48 +0200 Subject: [PATCH 4/8] Update psDscAdapter.psm1 for multi methods on export --- powershell-adapter/psDscAdapter/psDscAdapter.psm1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 5c470535..ff95b7ec 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -484,8 +484,16 @@ function Invoke-DscOperation { $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} } 'Export' { - $t = $dscResourceInstance.GetType() - $method = $t.GetMethod('Export') + $methods = $t.GetMethods() | Where-Object { $_.Name -eq 'Export' } + $method = foreach ($m in $methods) + { + if ($m.GetParameters().Count -eq 0) + { + $m + break + } + } + if ($null -eq $method) { "Export method not implemented by resource '$($DesiredState.Type)'" | Write-DscTrace -Operation Error exit 1 From d9bd59c257a74ed1719212e36253ba772b3f3730 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 24 Oct 2024 10:07:05 +0200 Subject: [PATCH 5/8] Add export class with constructor --- .../0.0.1/TestClassResource.psm1 | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 index 641cee6b..486e25a7 100644 --- a/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 +++ b/powershell-adapter/Tests/TestClassResource/0.0.1/TestClassResource.psm1 @@ -81,6 +81,30 @@ class TestClassResource : BaseTestClass return $resultList.ToArray() } + + static [TestClassResource[]] Export([bool]$UseExport) + { + if ($UseExport) + { + return [TestClassResource]::Export() + } + else + { + $resultList = [List[TestClassResource]]::new() + $resultCount = 5 + if ($env:TestClassResourceResultCount) { + $resultCount = $env:TestClassResourceResultCount + } + 1..$resultCount | %{ + $obj = New-Object TestClassResource + $obj.Name = "Object$_" + $obj.Prop1 = "Property of object$_" + $resultList.Add($obj) + } + } + + return $resultList.ToArray() + } } [DscResource()] From a969208760c3d037ad6069dcf0b21167347b667c Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 24 Oct 2024 10:35:46 +0200 Subject: [PATCH 6/8] Delete powershell-helpers directory --- powershell-helpers/README.md | 13 - powershell-helpers/dscCfgMigMod.psd1 | 47 --- powershell-helpers/dscCfgMigMod.psm1 | 384 ------------------ .../tests/dscCfgMigMod.tests.ps1 | 24 -- 4 files changed, 468 deletions(-) delete mode 100644 powershell-helpers/README.md delete mode 100644 powershell-helpers/dscCfgMigMod.psd1 delete mode 100644 powershell-helpers/dscCfgMigMod.psm1 delete mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md deleted file mode 100644 index d48f4e87..00000000 --- a/powershell-helpers/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: - -- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). - -## Getting started - -To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: - -1. Open a PowerShell terminal session -2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` -3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 deleted file mode 100644 index 83d1ac09..00000000 --- a/powershell-helpers/dscCfgMigMod.psd1 +++ /dev/null @@ -1,47 +0,0 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'dscCfgMigMod.psm1' - - # Version number of this module. - moduleVersion = '0.0.1' - - # ID used to uniquely identify this module - GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' - - # Author of this module - Author = 'Microsoft Corporation' - - # Company or vendor of this module - CompanyName = 'Microsoft Corporation' - - # Copyright statement for this module - Copyright = '(c) Microsoft Corporation. All rights reserved.' - - # Description of the functionality provided by this module - Description = 'PowerShell Desired State Configuration Migration Module helper' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('powershell-yaml') - - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - 'ConvertTo-DscJson' - 'ConvertTo-DscYaml' - ) - - # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() - - # Variables to export from this module - VariablesToExport = @() - - # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() - - PrivateData = @{ - PSData = @{ - ProjectUri = 'https://github.com/PowerShell/dsc' - } - } -} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 deleted file mode 100644 index c61ff4bf..00000000 --- a/powershell-helpers/dscCfgMigMod.psm1 +++ /dev/null @@ -1,384 +0,0 @@ -#region Main functions -function ConvertTo-DscJson -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 JSON format. - - .DESCRIPTION - The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscJson -Path $Path - - Returns: - { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", - "resources": { - "name": "TestResource", - "type": "Microsoft.DSC/PowerShell", - "properties": { - "resources": [ - { - "name": "Configure test resource", - "type": "TestResource/TestResource", - "properties": { - "Name": "MyTestResource", - "Ensure": "Absent" - } - } - ] - } - } - } - - .NOTES - Tags: DSC, Migration, JSON - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} - -function ConvertTo-DscYaml -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 YAML format. - - .DESCRIPTION - The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscYaml -Path $Path - - Returns: - $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - name: TestResource - type: Microsoft.DSC/PowerShell - properties: - resources: - - name: Configure test resource - type: TestResource/TestResource - properties: - Name: MyTestResource - Ensure: Absent - - .NOTES - Tags: DSC, Migration, YAML - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path -Format YAML - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} -#endRegion Main functions - -#region Helper functions -function FindAndExtractConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - if (-not (TestPathExtension $Path)) - { - return @{} - } - - # Parse the abstract syntax tree to get all hash table values representing the configuration resources - [System.Management.Automation.Language.Token[]] $tokens = $null - [System.Management.Automation.Language.ParseError[]] $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) - $configurations = $ast.FindAll({$args[0].GetType().Name -like 'HashtableAst'}, $true) - - # Create configuration document resource class (can be re-used) - $configurationDocument = [DscConfigurationResource]::new() - - # Build simple regex - $regex = [regex]::new('Configuration\s+(\w+)') - $configValue = $regex.Matches($ast.Extent.Text).Value - - if (-not $configValue) - { - return - } - - $documentConfigurationName = $configValue.TrimStart('Configuration').Trim(" ") - - # Start to build the outer basic format - $configurationDocument.name = $documentConfigurationName - $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type - - # Bag to hold resources - $resourceProps = [System.Collections.Generic.List[object]]::new() - - foreach ($configuration in $configurations) - { - # Get parent configuration details - $resourceName = ($configuration.Parent.CommandElements.Value | Select-Object -Last 1 ) - $resourceConfigurationName = ($configuration.Parent.CommandElements.Value | Select-Object -First 1) - - # Get module details - $module = Get-DscResource -Name $resourceConfigurationName -ErrorAction SilentlyContinue - - # Build the module - $resource = [DscConfigurationResource]::new() - $resource.properties = $configuration.SafeGetValue() - $resource.name = $resourceName - $resource.type = ("{0}/{1}" -f $module.ModuleName, $resourceConfigurationName) - # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource - # $document.ModuleName = $module.ModuleName - - Write-Verbose ("Adding document with data") - Write-Verbose ($resource | ConvertTo-Json | Out-String) - $resourceProps.Add($resource) - } - - # Add all the resources - $configurationDocument.properties = @{ - resources = $resourceProps - } - - return $configurationDocument -} - -function BuildConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path, - - [ValidateSet('JSON', 'YAML')] - [System.String] - $Format = 'JSON' - ) - - $configurationDocument = [ordered]@{ - "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder - resources = FindAndExtractConfigurationDocument -Path $Path - } - - switch ($Format) - { - "JSON" { - $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) - } - "YAML" { - if (TestYamlModule) - { - $inputObject = ($configurationDocument | ConvertTo-Yaml) - } - else - { - $inputObject = @{} - } - } - default { - $inputObject = $configurationDocument - } - } - - return $inputObject -} - -function TestPathExtension -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - $res = $true - - if (-not (Test-Path $Path)) - { - $res = $false - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".ps1")) - { - $res = $false - } - - return $res -} - -function TestYamlModule -{ - if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) - { - return $false - } - - return $true -} - -function GetPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\7\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function GetWindowsPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", - "$env:ProgramFiles\WindowsPowerShell\Modules", - "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function ResolvePowerShellPath -{ - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - if (-not (Test-Path $Path)) - { - return - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) - { - return - } - - if (GetPowerShellPath -Path $Path) - { - return "Microsoft.DSC/PowerShell" - } - - if (GetWindowsPowerShellPath -Path $Path) - { - return "Microsoft.Windows/WindowsPowerShell" - } - - return $null # TODO: Or default Microsoft.DSC/PowerShell -} - -#endRegion Helper functions - -#region Classes -class DscConfigurationResource -{ - [string] $name - [string] $type - [hashtable] $properties -} -#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 deleted file mode 100644 index b966993a..00000000 --- a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -Describe "DSC Configuration Migration Module tests" { - BeforeAll { - $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path - $modLoad = Import-Module $modPath -Force -PassThru - } - - Context "ConvertTo-DscYaml" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) - $res.resources | Should -BeNullOrEmpty - } - } - - Context "ConvertTo-DscJson" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) - $res.resources | Should -BeNullOrEmpty - } - } - - AfterAll { - Remove-Module -Name $modLoad.Name -Force - } -} From 7aca82ae4fc6b68db45501b2baf99ba6bdce655d Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 24 Oct 2024 10:37:07 +0200 Subject: [PATCH 7/8] Formatting --- powershell-adapter/psDscAdapter/psDscAdapter.psm1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index ff95b7ec..170d9450 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -485,12 +485,10 @@ function Invoke-DscOperation { } 'Export' { $methods = $t.GetMethods() | Where-Object { $_.Name -eq 'Export' } - $method = foreach ($m in $methods) - { - if ($m.GetParameters().Count -eq 0) - { + $method = foreach ($m in $methods) { + if ($m.GetParameters().Count -eq 0) { $m - break + break } } From 242a1a0a871ab41a7615f51ba3ee39e51855d4ff Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Sat, 26 Oct 2024 15:48:01 +0200 Subject: [PATCH 8/8] Fix Pester test --- powershell-adapter/psDscAdapter/psDscAdapter.psm1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 170d9450..2aad54cc 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -484,11 +484,12 @@ function Invoke-DscOperation { $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} } 'Export' { + $t = $dscResourceInstance.GetType() $methods = $t.GetMethods() | Where-Object { $_.Name -eq 'Export' } - $method = foreach ($m in $methods) { - if ($m.GetParameters().Count -eq 0) { - $m - break + $method = foreach ($mt in $methods) { + if ($mt.GetParameters().Count -eq 0) { + $mt + break } }