From 8f22f093e9173730bf8f59075e800d105161b212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raimund=20Andr=C3=A9e?= Date: Sat, 7 Mar 2026 19:58:08 +0100 Subject: [PATCH 1/3] Add support for conditional ResolutionPrecedence entries using InvokeCommand expressions (#167) - Implement filtering of null/empty path prefixes in Resolve-Datum. - Update documentation to reflect new conditional entry capabilities. - Add integration test for conditional ResolutionPrecedence in Datum.yml. --- CHANGELOG.md | 10 ++++++ docs/CmdletReference.md | 5 +++ docs/DatumYml.md | 34 +++++++++++++++++++ memory-bank/activeContext.md | 26 ++++++-------- source/Public/Resolve-Datum.ps1 | 1 + .../assets/DscWorkshopConfigData/Datum.yml | 11 ++++++ 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36c277..fc64b7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support conditional `ResolutionPrecedence` entries using + `Datum.InvokeCommand` expressions (`[x= ... =]`). Entries that + evaluate to `$null` or empty strings are now silently skipped + instead of causing lookup errors. + ### Changed - Remove `SkipReason` from RSOP test cases due to resolved merge logic bug. +- Add integration test for conditional `ResolutionPrecedence` entry + using an InvokeCommand expression that returns a path for some nodes + and nothing for others. ## [0.41.0] - 2026-02-03 diff --git a/docs/CmdletReference.md b/docs/CmdletReference.md index 2df73af..e07b4ab 100644 --- a/docs/CmdletReference.md +++ b/docs/CmdletReference.md @@ -101,6 +101,11 @@ Resolve-Datum -PropertyPath 'Configurations' -Variable $Node -DatumTree $Datum | `PathPrefixes` | `[string[]]` | No | `$DatumTree.__Definition.ResolutionPrecedence` | Resolution paths. Alias: `SearchPaths` | | `MaxDepth` | `[int]` | No | From definition or `-1` | Maximum merge recursion depth | +> **Note:** `PathPrefixes` are processed through configured datum +> handlers before lookup begins. Entries that resolve to `$null` or +> empty/whitespace strings (e.g. conditional `[x= ... =]` expressions) +> are automatically removed from the search list. + **Output:** `[System.Array]` — The resolved value(s). --- diff --git a/docs/DatumYml.md b/docs/DatumYml.md index 11b60c3..18355e8 100644 --- a/docs/DatumYml.md +++ b/docs/DatumYml.md @@ -156,6 +156,40 @@ Any property of the node hashtable can be referenced. > **Tip:** `$Node.Name` is the file name of the node data file (set automatically by the FileProvider). `$Node.NodeName` is typically set inside the data file itself, often using `'[x={ $Node.Name }=]'` via InvokeCommand to derive it from the file name. +#### Conditional Entries with InvokeCommand + +Resolution paths are processed through configured datum handlers +**before** lookup begins. This means you can use +`Datum.InvokeCommand` expressions (`[x= ... =]`) to include a path +only when a condition is met: + +```yaml +ResolutionPrecedence: + - AllNodes\$($Node.Environment)\$($Node.NodeName) + - Environment\$($Node.Environment) + - Roles\$($Node.Role) + - Baselines\Security + - | + '[x= { + if ($Node.Name -notlike "*file*") + { + "Somevalue" + } + else + { + # returns $null — entry is skipped + } + } =]' +``` + +When the expression returns `$null` or an empty string, the entry +is silently removed from the search list for that node. This keeps +the lookup clean without requiring separate precedence lists per +node type. + +> **Note:** The multi-line YAML block-scalar syntax (`|`) is required +> so the `[x= ... =]` marker spans multiple lines correctly. + #### Path Format - Use **backslash** (`\`) as the path separator diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index a76ec16..383adef 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,28 +1,22 @@ # Active Context -## Current State (as of 2026-02-25) -The project is on the `main` branch. Recent work has focused on **PR #154 improvements** — making the `ConvertFrom-Yaml` consolidation for JSON files more comprehensive with proper CHANGELOG categorization and integration tests for JSON/YAML equivalence. +## Current State (as of 2026-03-07) +The project is on the `feature/docs` branch. Recent work has focused on adding support for **conditional ResolutionPrecedence entries** using `Datum.InvokeCommand` expressions. ## Current Work Focus The project has **unreleased changes** (tracked in CHANGELOG.md under [Unreleased]) that include: ### Unreleased Features & Fixes -1. **Knockout support for basetype arrays** — New feature allowing items to be removed from inherited arrays -2. **Cleanup of knockout items** — Post-merge cleanup of knockout markers -3. **Pester tests for credential handling** — New test coverage -4. **ConvertTo-Datum fix** — Fixed returning `` when DatumHandler returns `False` (#139) -5. **Merge-DatumArray fix** — Fixed not returning array when merged array contains single hashtable -6. **Hashtable array merge fix** — Fixed items not merging when using datum handler for tuple keys (#155) -7. **Copy-Object fixes** — Fixed and extended tests, PowerShell 7 compatibility -8. **Pester 5 migration** — All integration tests migrated to Pester 5 syntax -9. **Build system update** — Updated to Sampler 0.119.0-preview0005 -10. **Merge-DatumArray improvement** — Convert tuple key values to datum before merging +1. **Conditional ResolutionPrecedence entries** — `Resolve-Datum` now filters out null/empty/whitespace path prefixes after datum handler invocation, enabling conditional `[x= ... =]` expressions in `ResolutionPrecedence` that can return nothing for certain nodes. +2. **Integration test for conditional precedence** — Added InvokeCommand expression to test `Datum.yml` that conditionally includes a path only for non-file-server nodes. +3. **SkipReason removal** — Removed `SkipReason` from RSOP test cases due to resolved merge logic bug. ## Recent Changes (Last Session) -- Moved CHANGELOG entry for PR #154 from `Added` to `Changed` with expanded description and PR link -- Created `tests/Integration/GetFileProviderData.tests.ps1` — 33 new tests for JSON/YAML equivalence -- Created test data in `tests/Integration/assets/JsonYamlEquivalence/` (JSON + YAML pairs) -- **Build result**: 191 passed, 0 failed, 3 skipped (pre-existing) +- Added `Where-Object { -not [string]::IsNullOrWhiteSpace($_) }` filter in `Resolve-Datum.ps1` after `ConvertTo-Datum` handler processing of `$PathPrefixes` +- Added conditional InvokeCommand expression to `tests/Integration/assets/DscWorkshopConfigData/Datum.yml` +- Updated CHANGELOG.md with Added and Changed entries under [Unreleased] +- Added "Conditional Entries with InvokeCommand" section to `docs/DatumYml.md` +- Added PathPrefixes filtering note to `docs/CmdletReference.md` ## Next Steps - ~~**Fix Issue #136**~~: DONE — depth now configurable via `default_json_depth` in Datum.yml (default 4), 8 tests, zero truncation warnings. Build verified, all tests passing. Ready for PR. diff --git a/source/Public/Resolve-Datum.ps1 b/source/Public/Resolve-Datum.ps1 index 93849ed..68636ed 100644 --- a/source/Public/Resolve-Datum.ps1 +++ b/source/Public/Resolve-Datum.ps1 @@ -173,6 +173,7 @@ function Resolve-Datum #Invoke datum handlers $PathPrefixes = $PathPrefixes | ConvertTo-Datum -DatumHandlers $datum.__Definition.DatumHandlers + $PathPrefixes = $PathPrefixes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } $allMergeResults = [System.Collections.Stack]::new() $Script:previousMergeResult = $null diff --git a/tests/Integration/assets/DscWorkshopConfigData/Datum.yml b/tests/Integration/assets/DscWorkshopConfigData/Datum.yml index a3c3f61..de21617 100644 --- a/tests/Integration/assets/DscWorkshopConfigData/Datum.yml +++ b/tests/Integration/assets/DscWorkshopConfigData/Datum.yml @@ -6,6 +6,17 @@ ResolutionPrecedence: - Baselines\Security - Baselines\$($Node.Baseline) - Baselines\DscLcm + - | + '[x= { + if ($node.Name -notlike "*file*") + { + 'Somevalue' + } + else + { + #nothing + } + } =]' DatumHandlersThrowOnError: false DatumHandlers: From b5d0cb9ac400fa3bcf45749513a6827d113ccf0d Mon Sep 17 00:00:00 2001 From: DoLearnWhileAlive <64268800+DoLearnWhileAlive@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:30:20 +0100 Subject: [PATCH 2/3] Add knockout support for hashtable array items (#161) * Fix CHANGELOG * Adjust integration tests for knockout of hashtable array items and for hashtable array merge behauvior 'Sum' * Add knockout support for hashtable array items and fix merging of hashtable array items using merge behaviour 'SUM' * Improve hash_array handling in Clear-DatumKnockout --- CHANGELOG.md | 28 +- source/Private/Clear-DatumKnockout.ps1 | 40 ++- source/Private/Merge-DatumArray.ps1 | 118 +++++++-- source/Public/Merge-Datum.ps1 | 5 +- tests/Integration/Merge.tests.ps1 | 13 +- tests/Integration/Rsop.tests.ps1 | 246 +++++++++++++++++- .../MergeTestData/AllNodes/DSCWeb01.yml | 6 + .../MergeTestData/AllNodes/DSCWeb02.yml | 25 ++ .../Baselines/ServerBaseline.yml | 43 +++ .../assets/MergeTestData/Datum.yml | 33 +++ .../assets/MergeTestData/Roles/FileServer.yml | 27 ++ .../assets/MergeTestData/Roles/WebServer.yml | 29 +++ 12 files changed, 531 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc64b7a..6805014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,22 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Support conditional `ResolutionPrecedence` entries using - `Datum.InvokeCommand` expressions (`[x= ... =]`). Entries that - evaluate to `$null` or empty strings are now silently skipped - instead of causing lookup errors. - -### Changed - -- Remove `SkipReason` from RSOP test cases due to resolved merge logic bug. -- Add integration test for conditional `ResolutionPrecedence` entry - using an InvokeCommand expression that returns a path for some nodes - and nothing for others. - -## [0.41.0] - 2026-02-03 - -### Added - - Add configurable `default_json_depth` setting in `Datum.yml` to control `ConvertTo-Json` serialization depth (default: `4`) ([#136](https://github.com/gaelcolas/datum/issues/136)). @@ -50,12 +34,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 RSOP.md. - Added AllNodes iteration examples for both flat and nested directory layouts in README.md, RSOP.md, and AboutDatum.md. +- Support conditional `ResolutionPrecedence` entries using + `Datum.InvokeCommand` expressions (`[x= ... =]`). Entries that + evaluate to `$null` or empty strings are now silently skipped + instead of causing lookup errors. +- Added knockout support for hashtable array items. ### Changed - Rewrote README.md with structured sections, table of contents, installation guide, merge strategy reference, handler documentation, and public function catalogue. +- Remove `SkipReason` from RSOP test cases due to resolved merge logic bug. +- Adjusted integration tests for knockout of hashtable array items. +- Adjusted integration tests for hashtable array merge behauvior 'Sum'. +- Add integration test for conditional `ResolutionPrecedence` entry + using an InvokeCommand expression that returns a path for some nodes + and nothing for others. ### Fixed @@ -71,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `$rsop.SomeKey.__source` pattern. - Documented `-IncludeSource`/`-RemoveSource` mutual exclusivity in RSOP.md and CmdletReference.md. +- Fixed merging of hashtable array items using merge behaviour 'Sum'. ## [0.41.0] - 2026-02-03 diff --git a/source/Private/Clear-DatumKnockout.ps1 b/source/Private/Clear-DatumKnockout.ps1 index 6b50850..9855c5e 100644 --- a/source/Private/Clear-DatumKnockout.ps1 +++ b/source/Private/Clear-DatumKnockout.ps1 @@ -117,28 +117,20 @@ function Clear-DatumKnockout { if ($strategy.merge_hash_array -match '^MostSpecific$|^First') { - return $ReferenceDatum + return , $ReferenceDatum } if ($knockoutPrefixMatcher = $strategy.merge_options.knockout_prefix) { - if ($tupleKeyNames = [string[]]$Strategy.merge_options.tuple_keys) + $knockoutPrefixMatcher = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^') + + if ($tupleKeyNames = [string[]]$strategy.merge_options.tuple_keys) { - $tupleKeyNames += $tupleKeyNames.ForEach{ $strategy.merge_options.knockout_prefix + $_ } $knockoutItems = foreach ($refItem in $ReferenceDatum) { - $refItemTupleKeys = $refItem.Keys.Where{ $_ -in $tupleKeyNames } - if ($refItemTupleKeys -match $knockoutPrefixMatcher) + if ($refItem.Keys.Where{ $_ -in $tupleKeyNames -and $refItem[$_] -match $knockoutPrefixMatcher }) { $refItem - $filterScript = @() - foreach ($refItemTupleKey in $refItemTupleKeys) - { - $refItemTupleKeyWithoutKnockoutPrefix = $refItemTupleKey -replace $knockoutPrefixMatcher, '' - $filterScript += "`$_.$refItemTupleKeyWithoutKnockoutPrefix -eq '$($refItem."$refItemTupleKey")'" - } - $filterScript = [scriptblock]::Create($filterScript -join ' -and ') - $ReferenceDatum.Where{ &$filterScript } } } foreach ($knockoutItem in $knockoutItems) @@ -148,19 +140,19 @@ function Clear-DatumKnockout } } - $cleanedArray = [System.Collections.ArrayList]::new() - - foreach ($currentItem in $ReferenceDatum) - { - $cleanupItemParams = @{ - StartingPath = $StartingPath - ReferenceDatum = $currentItem - Strategies = $Strategies + $cleanedArray = @( + foreach ($currentItem in $ReferenceDatum) + { + $cleanupItemParams = @{ + StartingPath = $StartingPath + ReferenceDatum = $currentItem + Strategies = $Strategies + } + Clear-DatumKnockout @cleanupItemParams } - $null = $cleanedArray.Add((Clear-DatumKnockout @cleanupItemParams)) - } + ) - return (, $cleanedArray) + return , $cleanedArray } default diff --git a/source/Private/Merge-DatumArray.ps1 b/source/Private/Merge-DatumArray.ps1 index 6668809..e5cfc0c 100644 --- a/source/Private/Merge-DatumArray.ps1 +++ b/source/Private/Merge-DatumArray.ps1 @@ -27,7 +27,6 @@ function Merge-DatumArray ) Write-Debug -Message "`tMerge-DatumArray -StartingPath <$StartingPath>" - $knockout_prefix = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^') $hashArrayStrategy = $Strategy.merge_hash_array Write-Debug -Message "`t`tHash Array Strategy: $hashArrayStrategy" $mergeBasetypeArraysStrategy = $Strategy.merge_basetype_array @@ -53,12 +52,52 @@ function Merge-DatumArray return $mergedArray } + $knockedOutTupleKeyValues = [System.Collections.ArrayList]@() + foreach ($referenceItem in $ReferenceArray) + { + $currentRefItem = [ordered]@{} + $referenceItem + + # make sure property values are converted before merge + $result = $null + foreach ($prop in $propertyNames.Where{ $currentRefItem.Contains($_) }) + { + if (Invoke-DatumHandler -InputObject $currentRefItem[$prop] -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result)) + { + $currentRefItem[$prop] = ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers + } + } + + if ($knockoutPrefixMatcher = $Strategy.merge_options.knockout_prefix) + { + $knockoutPrefixMatcher = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^') + + if ($tupleKeyNames = [string[]]$strategy.merge_options.tuple_keys) + { + if ($currentRefItemKeysWithKnockOutValues = $currentRefItem.Keys.Where{ $_ -in $tupleKeyNames -and $currentRefItem[$_] -match $knockoutPrefixMatcher }) + { + $ht = @{} + foreach ($key in $currentRefItemKeysWithKnockOutValues) + { + $ht.$key = $currentRefItem.$key -replace $knockoutPrefixMatcher + } + + $null = $knockedOutTupleKeyValues.Add($ht) + } + } + } + } + switch -Regex ($hashArrayStrategy) { '^Sum|^Add' { - (@($DifferenceArray) + @($ReferenceArray)) | ForEach-Object { - $null = $mergedArray.Add(([ordered]@{} + $_)) + foreach ($referenceItem in $ReferenceArray) + { + $null = $mergedArray.Add(([ordered]@{} + $referenceItem)) + } + foreach ($differenceItem in $DifferenceArray) + { + $null = $mergedArray.Add(([ordered]@{} + $differenceItem)) } } @@ -74,7 +113,7 @@ function Merge-DatumArray # if not found, add $DiffItem to $RefArray # look at each $RefItems in $RefArray - $usedDiffItems = [System.Collections.ArrayList]::new() + $usedOrKnockedOutDiffItems = [System.Collections.ArrayList]@() foreach ($referenceItem in $ReferenceArray) { $referenceItem = [ordered]@{} + $referenceItem @@ -85,18 +124,6 @@ function Merge-DatumArray Write-Debug -Message "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys" $propertyNames = $referenceItem.Keys } - # make sure property values are converted before merge - $result = $null - foreach ($prop in $propertyNames) - { - if ($referenceItem.Contains($prop)) - { - if (Invoke-DatumHandler -InputObject $referenceItem.$prop -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result)) - { - $referenceItem.$prop = ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers - } - } - } $mergedItem = @{} + $referenceItem $diffItemsToMerge = $DifferenceArray.Where{ $differenceItem = [ordered]@{} + $_ @@ -112,13 +139,31 @@ function Merge-DatumArray } } } - # Search for DiffItem that has the same Property/Value pairs than RefItem - $compareHashParams = @{ - ReferenceHashtable = [ordered]@{} + $referenceItem - DifferenceHashtable = $differenceItem - Property = $propertyNames + $itemKnockedOut = $false + foreach ($knockedOutTupleKeyValue in $knockedOutTupleKeyValues) + { + $filterStrings = foreach ($knockedOutTupleKeyValueKey in $knockedOutTupleKeyValue.Keys) + { + "`$knockedOutTupleKeyValue.'$knockedOutTupleKeyValueKey' -eq `$differenceItem.'$knockedOutTupleKeyValueKey'" + } + $filterScript = [scriptblock]::Create($filterStrings -join ' -and ') + if ( &$filterScript ) + { + $null = $usedOrKnockedOutDiffItems.Add($_) + $itemKnockedOut = $true + break + } + } + if ($itemKnockedOut -eq $false) + { + # Search for DiffItem that has the same Property/Value pairs than RefItem + $compareHashParams = @{ + ReferenceHashtable = [ordered]@{} + $referenceItem + DifferenceHashtable = $differenceItem + Property = $propertyNames + } + (-not (Compare-Hashtable @compareHashParams)) } - (-not (Compare-Hashtable @compareHashParams)) } Write-Debug -Message "`t`t`t ..Items to merge: $($diffItemsToMerge.Count)" $diffItemsToMerge | ForEach-Object { @@ -132,11 +177,11 @@ function Merge-DatumArray $mergedItem = Merge-Hashtable @mergeItemsParams } # If a diff Item has been used, save it to find the unused ones - $null = $usedDiffItems.AddRange($diffItemsToMerge) + $null = $usedOrKnockedOutDiffItems.AddRange($diffItemsToMerge) $null = $mergedArray.Add($mergedItem) } $unMergedItems = $DifferenceArray | ForEach-Object { - if (-not $usedDiffItems.Contains($_)) + if (-not $usedOrKnockedOutDiffItems.Contains($_)) { ([ordered]@{} + $_) } @@ -173,7 +218,7 @@ function Merge-DatumArray $mergedArray = [System.Collections.ArrayList]::new() $ReferenceArray | ForEach-Object { - $currentRefItem = $_ + $currentRefItem = [ordered]@{} + $_ # make sure property values are converted before merge $result = $null foreach ($prop in $propertyNames) @@ -193,7 +238,7 @@ function Merge-DatumArray } $DifferenceArray | ForEach-Object { - $currentDiffItem = $_ + $currentDiffItem = [ordered]@{} + $_ # make sure property values are converted before merge $result = $null foreach ($prop in $propertyNames) @@ -206,9 +251,26 @@ function Merge-DatumArray } } } - if (-not ($mergedArray.Where{ -not (Compare-Hashtable -Property $propertyNames -ReferenceHashtable $currentDiffItem -DifferenceHashtable $_ ) })) + $itemKnockedOut = $false + foreach ($knockedOutTupleKeyValue in $knockedOutTupleKeyValues) { - $null = $mergedArray.Add(([ordered]@{} + $_)) + $filterStrings = foreach ($knockedOutTupleKeyValueKey in $knockedOutTupleKeyValue.Keys) + { + "`$knockedOutTupleKeyValue.'$knockedOutTupleKeyValueKey' -eq `$currentDiffItem.'$knockedOutTupleKeyValueKey'" + } + $filterScript = [scriptblock]::Create($filterStrings -join ' -and ') + if ( &$filterScript ) + { + $itemKnockedOut = $true + break + } + } + if ($itemKnockedOut -eq $false) + { + if (-not ($mergedArray.Where{ -not (Compare-Hashtable -Property $propertyNames -ReferenceHashtable $currentDiffItem -DifferenceHashtable $_ ) })) + { + $null = $mergedArray.Add(([ordered]@{} + $_)) + } } } } diff --git a/source/Public/Merge-Datum.ps1 b/source/Public/Merge-Datum.ps1 index a06501d..f74eb12 100644 --- a/source/Public/Merge-Datum.ps1 +++ b/source/Public/Merge-Datum.ps1 @@ -190,10 +190,7 @@ function Merge-Datum '^Sum' { #--> $ref + $diff - (@($DifferenceArray) + @($ReferenceArray)).Foreach{ - $null = $MergedArray.Add(([ordered]@{} + $_)) - } - return (, $MergedArray) + return (, (Merge-DatumArray @MergeDatumArrayParams)) } Default diff --git a/tests/Integration/Merge.tests.ps1 b/tests/Integration/Merge.tests.ps1 index 5e3c18f..5b16d1e 100644 --- a/tests/Integration/Merge.tests.ps1 +++ b/tests/Integration/Merge.tests.ps1 @@ -53,21 +53,20 @@ Describe 'Merge ' { PropertyPath = 'WindowsFeatures\Name' Count = 4 } - @{ Node = 'DSCFile01' PropertyPath = 'Configurations' - Count = 3 + Count = 7 } @{ Node = 'DSCWeb01' PropertyPath = 'Configurations' - Count = 2 + Count = 6 } @{ Node = 'DSCWeb02' PropertyPath = 'Configurations' - Count = 3 + Count = 7 } ) @@ -82,17 +81,17 @@ Describe 'Merge ' { @{ Node = 'DSCFile01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures', 'FilesAndFolders' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures' + Value = 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb02' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'FilesAndFolders', 'WindowsFeatures' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCFile01' diff --git a/tests/Integration/Rsop.tests.ps1 b/tests/Integration/Rsop.tests.ps1 index 038dd82..d7a8b58 100644 --- a/tests/Integration/Rsop.tests.ps1 +++ b/tests/Integration/Rsop.tests.ps1 @@ -40,17 +40,17 @@ Describe "RSOP tests based on 'MergeTestData' test data" { @{ Node = 'DSCFile01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures', 'FilesAndFolders' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures' + Value = 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb02' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'FilesAndFolders', 'WindowsFeatures' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCFile01' @@ -147,6 +147,78 @@ Describe "RSOP tests based on 'MergeTestData' test data" { PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Count' Value = 4 } + # DSCFile01 - LocalUsers - LocalAdmin + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "LocalAdmin"}.Ensure' + Value = 'Present' + } + # DSCFile01 - LocalUsers - Admin1 + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "Admin1"}.Ensure' + Value = $null + } + # DSCFile01 - LocalUsers count + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.Count' + Value = 1 + } + # DSCFile01 - LocalUsers UserName's + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.UserName' + Value = 'LocalAdmin' + } + # DSCFile01 - RegistryValues - DevicesPickerUserSvc - Start + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "Start"}.Ensure' + Value = $null + } + # DSCFile01 - RegistryValues - DevicesPickerUserSvc - UserServiceFlags + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "UserServiceFlags"}.Ensure' + Value = 'Present' + } + # DSCFile01 - RegistryValues count + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Count' + Value = 3 + } + # DSCFile01 - RegistryValues UserName's + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Key' + Value = 'HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService' + } + # DSCFile01 - RegistryValues UserName's + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.ValueName' + Value = 'UserServiceFlags', 'Start', 'UserServiceFlags' + } + # DSCFile01 - Accounts_Rename_administrator_account + @{ + Node = 'DSCFile01' + PropertyPath = 'SecurityOptions.Policies.Where{$_.Name -eq "Accounts_Rename_administrator_account"}.Accounts_Rename_administrator_account' + Value = 'LocalAdmin' + } + # DSCFile01 - SummaryConfig count + @{ + Node = 'DSCFile01' + PropertyPath = 'SummaryConfig.SumItems.Count' + Value = 3 + } + # DSCFile01 - SummaryConfig ItemNumbers + @{ + Node = 'DSCFile01' + PropertyPath = 'SummaryConfig.SumItems.ItemNumber' + Value = 1, 2, 3 + } # DSCWeb01 - Ethernet 1 @{ Node = 'DSCWeb01' @@ -158,6 +230,90 @@ Describe "RSOP tests based on 'MergeTestData' test data" { PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 1"}.Gateway' Value = $null } + # DSCWeb01 - Interface count + @{ + Node = 'DSCWeb01' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Count' + Value = 2 + } + # DSCWeb01 - Ethernet 2 + @{ + Node = 'DSCWeb01' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 2"}' + Value = $null + } + # DSCWeb01 - LocalUsers - LocalAdmin + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "LocalAdmin"}.Ensure' + Value = $null + } + # DSCWeb01 - LocalUsers - Admin1 + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "Admin1"}.Ensure' + Value = $null + } + # DSCWeb01 - LocalUsers count + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.Count' + Value = 0 + } + # DSCWeb01 - LocalUsers UserName's + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.UserName' + Value = $null + } + # DSCWeb01 - RegistryValues - DevicesPickerUserSvc - Start + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "Start"}.Ensure' + Value = $null + } + # DSCWeb01 - RegistryValues - DevicesPickerUserSvc - UserServiceFlags + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "UserServiceFlags"}.Ensure' + Value = $null + } + # DSCWeb01 - RegistryValues count + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Count' + Value = 2 + } + # DSCWeb01 - RegistryValues UserName's + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Key' + Value = 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService' + } + # DSCWeb01 - RegistryValues UserName's + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.ValueName' + Value = 'Start', 'UserServiceFlags' + } + # DSCWeb01 - Accounts_Rename_administrator_account + @{ + Node = 'DSCWeb01' + PropertyPath = 'SecurityOptions.Policies.Where{$_.Name -eq "Accounts_Rename_administrator_account"}.Accounts_Rename_administrator_account' + Value = $null + } + # DSCWeb01 - SummaryConfig count + @{ + Node = 'DSCWeb01' + PropertyPath = 'SummaryConfig.SumItems.Count' + Value = 3 + } + # DSCWeb01 - SummaryConfig ItemNumbers + @{ + Node = 'DSCWeb01' + PropertyPath = 'SummaryConfig.SumItems.ItemNumber' + Value = 1, 2, 4 + } # DSCWeb02 - Ethernet 1 @{ Node = 'DSCWeb02' @@ -169,6 +325,90 @@ Describe "RSOP tests based on 'MergeTestData' test data" { PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 1"}.Gateway' Value = '192.168.10.50' } + # DSCWeb02 - Interface count + @{ + Node = 'DSCWeb02' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Count' + Value = 2 + } + # DSCWeb02 - Ethernet 2 + @{ + Node = 'DSCWeb02' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 2"}' + Value = $null + } + # DSCWeb02 - LocalUsers - LocalAdmin + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "LocalAdmin"}.Ensure' + Value = 'Absent' + } + # DSCWeb02 - LocalUsers - Admin1 + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "Admin1"}.Ensure' + Value = 'Present' + } + # DSCWeb02 - LocalUsers count + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.Count' + Value = 2 + } + # DSCWeb02 - LocalUsers UserName's + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.UserName' + Value = 'LocalAdmin', 'Admin1' + } + # DSCWeb02 - RegistryValues - DevicesPickerUserSvc - Start + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "Start"}.Ensure' + Value = 'Absent' + } + # DSCWeb02 - RegistryValues - DevicesPickerUserSvc - UserServiceFlags + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "UserServiceFlags"}.Ensure' + Value = $null + } + # DSCWeb02 - RegistryValues count + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Count' + Value = 3 + } + # DSCWeb02 - RegistryValues UserName's + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Key' + Value = 'HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService' + } + # DSCWeb02 - RegistryValues UserName's + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.ValueName' + Value = 'Start', 'Start', 'UserServiceFlags' + } + # DSCWeb02 - Accounts_Rename_administrator_account + @{ + Node = 'DSCWeb02' + PropertyPath = 'SecurityOptions.Policies.Where{$_.Name -eq "Accounts_Rename_administrator_account"}.Accounts_Rename_administrator_account' + Value = 'Admin1' + } + # DSCWeb02 - SummaryConfig count + @{ + Node = 'DSCWeb02' + PropertyPath = 'SummaryConfig.SumItems.Count' + Value = 2 + } + # DSCWeb02 - SummaryConfig ItemNumbers + @{ + Node = 'DSCWeb02' + PropertyPath = 'SummaryConfig.SumItems.ItemNumber' + Value = 1, 2 + } ) It "The value of Datum RSOP property '' for node '' should be ''." -ForEach $script:testCases { diff --git a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml index dc6a7e1..0046e1c 100644 --- a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml +++ b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml @@ -44,3 +44,9 @@ LcmConfig: ConfigurationRepositoryWeb: Server: ConfigurationNames: DSCWeb01 + +SummaryConfig: + SumItems: + - ItemName: Item1 + ItemNumber: 4 + Decription: Item1 with number 4 diff --git a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml index 415a375..c1786cb 100644 --- a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml +++ b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml @@ -12,6 +12,18 @@ WindowsFeatures: Configurations: - FilesAndFolders +LocalUsers: + Users: + - UserName: Admin1 + Ensure: Present + Disabled: false + PasswordChangeRequired: false + PasswordNeverExpires: false + - UserName: LocalAdmin + Ensure: Absent + DependsOn: + - '[SecurityOption]secPolOpt_Accounts_Rename_administrator_account::[SecurityOptions]SecurityOptions' + NetworkIpConfigurationMerged: Interfaces: - InterfaceAlias: Ethernet 1 @@ -22,6 +34,19 @@ NetworkIpConfigurationNonMerged: - InterfaceAlias: Ethernet 1 IpAddress: 192.168.10.102 +RegistryValues: + Values: + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: Start + Ensure: Absent + Force: true + +SecurityOptions: + Policies: + - Name: Accounts_Rename_administrator_account + Accounts_Rename_administrator_account: Admin1 + + PSDscAllowPlainTextPassword: True PSDscAllowDomainUser: True diff --git a/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml b/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml index 93b5cb9..0dfec02 100644 --- a/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml +++ b/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml @@ -1,6 +1,10 @@ Configurations: - WindowsFeatures - NetworkIpConfigurationMerged + - LocalUsers + - RegistryValues + - SecurityOptions + - SummaryConfig WindowsFeatures: Name: @@ -39,3 +43,42 @@ NetworkIpConfigurationMerged: SecurityBase: Role: Baseline + +LocalUsers: + Users: + - UserName: LocalAdmin + Ensure: Present + Disabled: false + PasswordChangeRequired: false + PasswordNeverExpires: false + DependsOn: + - '[SecurityOption]secPolOpt_Accounts_Rename_administrator_account::[SecurityOptions]SecurityOptions' + +RegistryValues: + Values: + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: UserServiceFlags + ValueData: 0 + ValueType: Dword + Ensure: Present + Force: true + +SecurityOptions: + Policies: + - Name: Accounts_Rename_administrator_account + Accounts_Rename_administrator_account: LocalAdmin + +SummaryConfig: + SumItems: + - ItemName: Item1 + ItemNumber: 1 + Decription: Item1 with number 1 + - ItemName: Item1 + ItemNumber: 2 + Decription: Item1 with number 2 diff --git a/tests/Integration/assets/MergeTestData/Datum.yml b/tests/Integration/assets/MergeTestData/Datum.yml index 510b8d2..272a4b5 100644 --- a/tests/Integration/assets/MergeTestData/Datum.yml +++ b/tests/Integration/assets/MergeTestData/Datum.yml @@ -17,6 +17,15 @@ lookup_options: merge_options: knockout_prefix: -- + LocalUsers: + merge_hash: deep + LocalUsers\Users: + merge_hash_array: DeepTuple + merge_options: + knockout_prefix: -- + tuple_keys: + - UserName + NetworkIpConfigurationMerged: merge_hash: deep merge_options: @@ -31,3 +40,27 @@ lookup_options: merge_basetype_array: Unique merge_options: knockout_prefix: -- + + RegistryValues: + merge_hash: deep + RegistryValues\Values: + merge_hash_array: UniqueKeyValTuples + merge_options: + knockout_prefix: -- + tuple_keys: + - Key + - ValueName + + SecurityOptions: + merge_hash: deep + SecurityOptions\Policies: + merge_hash_array: UniqueKeyValTuples + merge_options: + knockout_prefix: -- + tuple_keys: + - Name + + SummaryConfig: + merge_hash: deep + SummaryConfig\SumItems: + merge_hash_array: Sum diff --git a/tests/Integration/assets/MergeTestData/Roles/FileServer.yml b/tests/Integration/assets/MergeTestData/Roles/FileServer.yml index b529a9d..18a4cba 100644 --- a/tests/Integration/assets/MergeTestData/Roles/FileServer.yml +++ b/tests/Integration/assets/MergeTestData/Roles/FileServer.yml @@ -31,3 +31,30 @@ NetworkIpConfigurationNonMerged: Gateway: 192.168.10.50 DnsServer: 192.168.10.10 DisableNetbios: True + +RegistryValues: + Values: + - Key: --HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: --Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: UserServiceFlags + ValueData: 0 + ValueType: Dword + Ensure: Present + Force: true + +SummaryConfig: + SumItems: + - ItemName: Item1 + ItemNumber: 3 + Decription: Item1 with number 3 diff --git a/tests/Integration/assets/MergeTestData/Roles/WebServer.yml b/tests/Integration/assets/MergeTestData/Roles/WebServer.yml index 42e8e42..bb14fed 100644 --- a/tests/Integration/assets/MergeTestData/Roles/WebServer.yml +++ b/tests/Integration/assets/MergeTestData/Roles/WebServer.yml @@ -1,5 +1,7 @@ Configurations: - WindowsFeatures + - NetworkIpConfigurationMerged + - LocalUsers WindowsFeatures: Name: @@ -18,6 +20,7 @@ NetworkIpConfigurationMerged: - 192.168.12.0/24 - 192.168.23.0/24 - 192.168.34.0/24 + - InterfaceAlias: --Ethernet 2 NetworkIpConfigurationNonMerged: ConfigureIPv6: -1 @@ -27,3 +30,29 @@ NetworkIpConfigurationNonMerged: Gateway: 192.168.10.50 DnsServer: 192.168.10.10 DisableNetbios: True + +LocalUsers: + Users: + - UserName: --LocalAdmin + DependsOn: + - '--[SecurityOption]secPolOpt_Accounts_Rename_administrator_account::[SecurityOptions]SecurityOptions' + +RegistryValues: + Values: + - Key: --HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: UserServiceFlags + ValueData: 0 + ValueType: Dword + Ensure: Present + Force: true + +SecurityOptions: + Policies: + - Name: --Accounts_Rename_administrator_account From 720e77bc11c1c7c5ca5646ce7835c397bcd387d0 Mon Sep 17 00:00:00 2001 From: Heiko Stehli Date: Sun, 1 May 2022 13:30:56 +0200 Subject: [PATCH 3/3] prevent early resolving of Powershell variables by using .NET Combine instead of Join-Path --- source/Public/Resolve-Datum.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Public/Resolve-Datum.ps1 b/source/Public/Resolve-Datum.ps1 index 68636ed..8188883 100644 --- a/source/Public/Resolve-Datum.ps1 +++ b/source/Public/Resolve-Datum.ps1 @@ -183,7 +183,7 @@ function Resolve-Datum { #through the hierarchy $arraySb = [System.Collections.ArrayList]@() - $currentSearch = Join-Path -Path $searchPrefix -ChildPath $PropertyPath + $currentSearch = [System.IO.Path]::Combine($searchPrefix, $PropertyPath) Write-Verbose -Message '' Write-Verbose -Message " Lookup <$currentSearch> $($Node.Name)" #extract script block for execution into array, replace by substition strings {0},{1}...