From 3d98c62a242c445980b8e095904c7997c7006385 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 24 Jul 2024 13:22:19 +0200 Subject: [PATCH 1/6] 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/6] 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/6] 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 dfe6d9b7bf3a46b500eb117385c31d2081a1e142 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Mon, 30 Sep 2024 11:59:26 +0200 Subject: [PATCH 4/6] WIP schema option in PsDscAdapter --- .../psDscAdapter/powershell.resource.ps1 | 162 +++++++++++++----- 1 file changed, 121 insertions(+), 41 deletions(-) diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index acaafad5..38341865 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -3,13 +3,14 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate.')] - [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'ClearCache')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate', 'Schema', 'ClearCache')] [string]$Operation, [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] [string]$jsonInput = '@{}' ) -function Write-DscTrace { +function Write-DscTrace +{ param( [Parameter(Mandatory = $false)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] @@ -27,34 +28,44 @@ function Write-DscTrace { 'PSPath=' + $PSHome | Write-DscTrace 'PSModulePath=' + $env:PSModulePath | Write-DscTrace -if ($Operation -eq 'ClearCache') { - $cacheFilePath = if ($IsWindows) { - # PS 6+ on Windows - Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } else { - # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) { - Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - } else { - Join-Path $env:HOME ".dsc" "PSAdapterCache.json" - } +$cacheFilePath = if ($IsWindows) +{ + # PS 6+ on Windows + Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" +} +else +{ + # either WinPS or PS 6+ on Linux/Mac + if ($PSVersionTable.PSVersion.Major -le 5) + { + Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" + } + else + { + Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } +} +if ($Operation -eq 'ClearCache') +{ 'Deleting cache file ' + $cacheFilePath | Write-DscTrace Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath exit 0 } -if ('Validate' -ne $Operation) { +if ('Validate' -ne $Operation) +{ # write $jsonInput to STDERR for debugging $trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) # load private functions of psDscAdapter stub module - if ($PSVersionTable.PSVersion.Major -le 5) { + if ($PSVersionTable.PSVersion.Major -le 5) + { $psDscAdapter = Import-Module "$PSScriptRoot/win_psDscAdapter.psd1" -Force -PassThru } - else { + else + { $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru } @@ -62,8 +73,10 @@ if ('Validate' -ne $Operation) { $result = [System.Collections.Generic.List[Object]]::new() } -if ($jsonInput) { - if ($jsonInput -ne '@{}') { +if ($jsonInput) +{ + if ($jsonInput -ne '@{}') + { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath @@ -74,45 +87,56 @@ if ($jsonInput) { } # process the operation requested to the script -switch ($Operation) { - 'List' { +switch ($Operation) +{ + 'List' + { $dscResourceCache = Invoke-DscCacheRefresh # cache was refreshed on script load - foreach ($dscResource in $dscResourceCache) { - + foreach ($dscResource in $dscResourceCache) + { + # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo $DscResourceInfo = $dscResource.DscResourceInfo # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test # TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module - if ($DscResourceInfo.ModuleName) { + if ($DscResourceInfo.ModuleName) + { $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 - if ($module.PrivateData.PSData.DscCapabilities) { + if ($module.PrivateData.PSData.DscCapabilities) + { $capabilities = $module.PrivateData.PSData.DscCapabilities } - else { + else + { $capabilities = @('Get', 'Set', 'Test') } } # this text comes directly from the resource manifest for v3 native resources - if ($DscResourceInfo.Description) { + if ($DscResourceInfo.Description) + { $description = $DscResourceInfo.Description } - elseif ($module.Description) { + elseif ($module.Description) + { # some modules have long multi-line descriptions. to avoid issue, use only the first line. $description = $module.Description.split("`r`n")[0] } - else { + else + { $description = '' } # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) { + if ($PSVersionTable.PSVersion.Major -le 5) + { $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' } - else { + else + { $requireAdapter = 'Microsoft.DSC/PowerShell' } @@ -132,9 +156,11 @@ switch ($Operation) { } | ConvertTo-Json -Compress } } - { @('Get','Set','Test','Export') -contains $_ } { + { @('Get', 'Set', 'Test', 'Export') -contains $_ } + { $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) - if ($null -eq $desiredState) { + if ($null -eq $desiredState) + { $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 @@ -142,49 +168,93 @@ switch ($Operation) { # only need to cache the resources that are used $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } - if ($null -eq $dscResourceModules) { + if ($null -eq $dscResourceModules) + { $trace = @{'Debug' = 'ERROR: Could not get list of DSC resource types from provided JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules - if ($dscResourceCache.count -lt $dscResourceModules.count) { + if ($dscResourceCache.count -lt $dscResourceModules.count) + { $trace = @{'Debug' = 'ERROR: DSC resource module not found.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } - foreach ($ds in $desiredState) { + foreach ($ds in $desiredState) + { # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $ds, $dscResourceCache) - if ($null -eq $actualState) { + if ($null -eq $actualState) + { $trace = @{'Debug' = 'ERROR: Incomplete GET for resource ' + $ds.Name } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } $result += $actualState } - + # OUTPUT json to stderr for debug, and to stdout $result = @{ result = $result } | ConvertTo-Json -Depth 10 -Compress $trace = @{'Debug' = 'jsonOutput=' + $result } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) return $result } - 'Validate' { + 'Schema' + { + $cache = Get-Content $cacheFilePath | ConvertFrom-Json + + # TODO: Validate how input is passed + $resourceInfoproperties = ($cache.ResourceCache | Where-Object { $_.Type -eq $_.Type }).DscResourceInfo.Properties + + $props = @{} + $resourceInfoproperties | Foreach-Object { + if ($_.IsMandatory -eq $true) + { + $props[$_.Name] = [hashtable]@{ + type = $_.PropertyType + description = "" + } + } + else + { + $props[$_.Name] = [hashtable]@{ + type = @($_.PropertyType, $null) + description = "" + } + } + } + + $out = [resourceProperties]@{ + schema = 'http://json-schema.org/draft-04/schema#' + title = $jsonInput.Type + type = 'object' + required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name + properties = $props + additionalProperties = $false + # definitions = $null # TODO: Should we add definitions + } + + $out | ConvertTo-Json -Depth 10 -Compress + } + 'Validate' + { # VALIDATE not implemented - + # OUTPUT @{ valid = $true } | ConvertTo-Json } - Default { + Default + { Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' } } # output format for resource list -class resourceOutput { +class resourceOutput +{ [string] $type [string] $kind [string] $version @@ -197,3 +267,13 @@ class resourceOutput { [string] $requireAdapter [string] $description } + +class resourceProperties +{ + [string] $schema + [string] $title + [string] $type + [string[]] $required + [hashtable] $properties + [bool] $additionalProperties +} \ No newline at end of file From 4b1ef8eb609589e251eb9f2a17ca2e8d78ec7fdf Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Mon, 30 Sep 2024 12:00:43 +0200 Subject: [PATCH 5/6] Remove 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 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 529b9614771fd9ca392332b4ca1beea72e400997 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 2 Oct 2024 04:45:56 +0200 Subject: [PATCH 6/6] Added test --- .vscode/settings.json | 1 + .../testclassresource.dsc.resource.json | 22 +++ .../Tests/powershellgroup.resource.tests.ps1 | 17 +++ .../psDscAdapter/powershell.resource.ps1 | 132 ++++++------------ 4 files changed, 84 insertions(+), 88 deletions(-) create mode 100644 powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 54f38dc4..799f6e4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "./y2j/Cargo.toml" ], "rust-analyzer.showUnlinkedFileNotification": true, + "powershell.codeFormatting.preset": "OTBS", "json.schemas": [ { "fileMatch": ["**/*.dsc.resource.json"], diff --git a/powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json b/powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json new file mode 100644 index 00000000..0b34c327 --- /dev/null +++ b/powershell-adapter/Tests/TestClassResource/testclassresource.dsc.resource.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "TestClassResource/TestClassResource", + "description": "Manage test class", + "tags": [ + "Windows" + ], + "version": "0.1.0", + "schema": { + "command": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./psDscAdapter/powershell.resource.ps1 Schema" + ], + "input": "stdin" + } + } +} diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index a15e6b4c..c7e2df0e 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -325,4 +325,21 @@ Describe 'PowerShell adapter resource tests' { "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' } } + + It "Verify that Schema operation works on PS class-based resource" { + BeforeDiscovery { + $resourceManifest = Resolve-Path -Path (Join-Path $PSScriptRoot 'TestClassResource' 'testclassresource.dsc.resource.json') + $dest = Split-Path -Path ((Get-Command dsc).Source) -Parent + $script:file = Copy-Item -Path $resourceManifest -Destination $dest -Force -PassThru + } + + $r = dsc resource schema --resource TestClassResource/TestClassResource + $properties = $r | ConvertFrom-Json + $properties.required | Should -Not -BeNullOrEmpty + $properties.properties.PSObject.properties.Name.Contains('BaseProperty') | Should -BeTrue + } } + +AfterAll { + Remove-Item -Path $file.FullName -Force -ErrorAction SilentlyContinue +} \ No newline at end of file diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index 38341865..8ace4130 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -9,8 +9,7 @@ param( [string]$jsonInput = '@{}' ) -function Write-DscTrace -{ +function Write-DscTrace { param( [Parameter(Mandatory = $false)] [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] @@ -28,44 +27,33 @@ function Write-DscTrace 'PSPath=' + $PSHome | Write-DscTrace 'PSModulePath=' + $env:PSModulePath | Write-DscTrace -$cacheFilePath = if ($IsWindows) -{ +$cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" -} -else -{ +} else { # either WinPS or PS 6+ on Linux/Mac - if ($PSVersionTable.PSVersion.Major -le 5) - { + if ($PSVersionTable.PSVersion.Major -le 5) { Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - } - else - { + } else { Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } } -if ($Operation -eq 'ClearCache') -{ +if ($Operation -eq 'ClearCache') { 'Deleting cache file ' + $cacheFilePath | Write-DscTrace Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath exit 0 } -if ('Validate' -ne $Operation) -{ +if ('Validate' -ne $Operation) { # write $jsonInput to STDERR for debugging $trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) # load private functions of psDscAdapter stub module - if ($PSVersionTable.PSVersion.Major -le 5) - { + if ($PSVersionTable.PSVersion.Major -le 5) { $psDscAdapter = Import-Module "$PSScriptRoot/win_psDscAdapter.psd1" -Force -PassThru - } - else - { + } else { $psDscAdapter = Import-Module "$PSScriptRoot/psDscAdapter.psd1" -Force -PassThru } @@ -73,70 +61,52 @@ if ('Validate' -ne $Operation) $result = [System.Collections.Generic.List[Object]]::new() } -if ($jsonInput) -{ - if ($jsonInput -ne '@{}') - { +if ($jsonInput) { + if ($jsonInput -ne '@{}') { $inputobj_pscustomobj = $jsonInput | ConvertFrom-Json } $new_psmodulepath = $inputobj_pscustomobj.psmodulepath - if ($new_psmodulepath) - { + if ($new_psmodulepath) { $env:PSModulePath = $ExecutionContext.InvokeCommand.ExpandString($new_psmodulepath) } } # process the operation requested to the script -switch ($Operation) -{ - 'List' - { +switch ($Operation) { + 'List' { $dscResourceCache = Invoke-DscCacheRefresh # cache was refreshed on script load - foreach ($dscResource in $dscResourceCache) - { + foreach ($dscResource in $dscResourceCache) { # https://learn.microsoft.com/dotnet/api/system.management.automation.dscresourceinfo $DscResourceInfo = $dscResource.DscResourceInfo # Provide a way for existing resources to specify their capabilities, or default to Get, Set, Test # TODO: for perf, it is better to take capabilities from psd1 in Invoke-DscCacheRefresh, not by extra call to Get-Module - if ($DscResourceInfo.ModuleName) - { + if ($DscResourceInfo.ModuleName) { $module = Get-Module -Name $DscResourceInfo.ModuleName -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 - if ($module.PrivateData.PSData.DscCapabilities) - { + if ($module.PrivateData.PSData.DscCapabilities) { $capabilities = $module.PrivateData.PSData.DscCapabilities - } - else - { + } else { $capabilities = @('Get', 'Set', 'Test') } } # this text comes directly from the resource manifest for v3 native resources - if ($DscResourceInfo.Description) - { + if ($DscResourceInfo.Description) { $description = $DscResourceInfo.Description - } - elseif ($module.Description) - { + } elseif ($module.Description) { # some modules have long multi-line descriptions. to avoid issue, use only the first line. $description = $module.Description.split("`r`n")[0] - } - else - { + } else { $description = '' } # match adapter to version of powershell - if ($PSVersionTable.PSVersion.Major -le 5) - { + if ($PSVersionTable.PSVersion.Major -le 5) { $requireAdapter = 'Microsoft.Windows/WindowsPowerShell' - } - else - { + } else { $requireAdapter = 'Microsoft.DSC/PowerShell' } @@ -156,11 +126,9 @@ switch ($Operation) } | ConvertTo-Json -Compress } } - { @('Get', 'Set', 'Test', 'Export') -contains $_ } - { + { @('Get', 'Set', 'Test', 'Export') -contains $_ } { $desiredState = $psDscAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) - if ($null -eq $desiredState) - { + if ($null -eq $desiredState) { $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 @@ -168,27 +136,23 @@ switch ($Operation) # only need to cache the resources that are used $dscResourceModules = $desiredState | ForEach-Object { $_.Type.Split('/')[0] } - if ($null -eq $dscResourceModules) - { + if ($null -eq $dscResourceModules) { $trace = @{'Debug' = 'ERROR: Could not get list of DSC resource types from provided JSON.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } $dscResourceCache = Invoke-DscCacheRefresh -module $dscResourceModules - if ($dscResourceCache.count -lt $dscResourceModules.count) - { + if ($dscResourceCache.count -lt $dscResourceModules.count) { $trace = @{'Debug' = 'ERROR: DSC resource module not found.' } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 } - foreach ($ds in $desiredState) - { + foreach ($ds in $desiredState) { # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState $actualState = $psDscAdapter.invoke( { param($op, $ds, $dscResourceCache) Invoke-DscOperation -Operation $op -DesiredState $ds -dscResourceCache $dscResourceCache }, $Operation, $ds, $dscResourceCache) - if ($null -eq $actualState) - { + if ($null -eq $actualState) { $trace = @{'Debug' = 'ERROR: Incomplete GET for resource ' + $ds.Name } | ConvertTo-Json -Compress $host.ui.WriteErrorLine($trace) exit 1 @@ -202,24 +166,20 @@ switch ($Operation) $host.ui.WriteErrorLine($trace) return $result } - 'Schema' - { + 'Schema' { $cache = Get-Content $cacheFilePath | ConvertFrom-Json - # TODO: Validate how input is passed - $resourceInfoproperties = ($cache.ResourceCache | Where-Object { $_.Type -eq $_.Type }).DscResourceInfo.Properties + # TODO: Validate how input is passed and remove hindden properties + $resourceInfoproperties = ($cache.ResourceCache | Where-Object { $_.Type -eq 'TestClassResource/TestClassResource' }).DscResourceInfo.Properties $props = @{} $resourceInfoproperties | Foreach-Object { - if ($_.IsMandatory -eq $true) - { + if ($_.IsMandatory -eq $true) { $props[$_.Name] = [hashtable]@{ type = $_.PropertyType description = "" } - } - else - { + } else { $props[$_.Name] = [hashtable]@{ type = @($_.PropertyType, $null) description = "" @@ -228,33 +188,30 @@ switch ($Operation) } $out = [resourceProperties]@{ - schema = 'http://json-schema.org/draft-04/schema#' - title = $jsonInput.Type - type = 'object' - required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name - properties = $props + schema = 'http://json-schema.org/draft-04/schema#' + title = ($cache.ResourceCache | Where-Object { $_.Type -eq 'TestClassResource/TestClassResource' }).Type + type = 'object' + required = @($resourceInfoproperties | Where-Object { $_.IsMandatory -eq $true }).Name + properties = $props additionalProperties = $false # definitions = $null # TODO: Should we add definitions } $out | ConvertTo-Json -Depth 10 -Compress } - 'Validate' - { + 'Validate' { # VALIDATE not implemented # OUTPUT @{ valid = $true } | ConvertTo-Json } - Default - { - Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + Default { + Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Schema, Validate' } } # output format for resource list -class resourceOutput -{ +class resourceOutput { [string] $type [string] $kind [string] $version @@ -268,8 +225,7 @@ class resourceOutput [string] $description } -class resourceProperties -{ +class resourceProperties { [string] $schema [string] $title [string] $type