Datum.psm1
Class FileProvider { hidden $Path hidden [hashtable] $Store hidden [hashtable] $DatumHierarchyDefinition hidden [hashtable] $StoreOptions hidden [hashtable] $DatumHandlers FileProvider ($Path,$Store,$DatumHierarchyDefinition) { $this.Store = $Store $this.DatumHierarchyDefinition = $DatumHierarchyDefinition $this.StoreOptions = $Store.StoreOptions $this.Path = Get-Item $Path -ErrorAction SilentlyContinue $this.DatumHandlers = $DatumHierarchyDefinition.DatumHandlers $Result = Get-ChildItem $path | ForEach-Object { if($_.PSisContainer) { $val = [scriptblock]::Create("New-DatumFileProvider -Path `"$($_.FullName)`" -StoreOptions `$this.DataOptions -DatumHierarchyDefinition `$this.DatumHierarchyDefinition") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } else { $val = [scriptblock]::Create("Get-FileProviderData -Path `"$($_.FullName)`" -DatumHandlers `$this.DatumHandlers") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } } } } Class Node : hashtable { Node([hashtable]$NodeData) { $NodeData.keys | % { $This[$_] = $NodeData[$_] } $this | Add-member -MemberType ScriptProperty -Name Roles -Value { $PathArray = $ExecutionContext.InvokeCommand.InvokeScript('Get-PSCallStack')[2].Position.text -split '\.' $PropertyPath = $PathArray[2..($PathArray.count-1)] -join '\' Write-warning "Resolve $PropertyPath" $obj = [PSCustomObject]@{} $currentNode = $obj if($PathArray.Count -gt 3) { foreach ($property in $PathArray[2..($PathArray.count-2)]) { Write-Debug "Adding $Property property" $currentNode | Add-member -MemberType NoteProperty -Name $property -Value ([PSCustomObject]@{}) $currentNode = $currentNode.$property } } Write-Debug "Adding Resolved property to last object's property $($PathArray[-1])" $currentNode | Add-member -MemberType NoteProperty -Name $PathArray[-1] -Value ($PropertyPath) return $obj } } static ResolveDscProperty($Path) { "Resolve-DscProperty $Path" } } Class SecureDatum { [hashtable] hidden $UnprotectParams SecureDatum($Object,[hashtable]$UnprotectParams) { $this.UnprotectParams = $UnprotectParams if($Object -is [hashtable]) { $Object = [PSCustomObject]$Object } if ($Object -is [PSCustomObject]) { foreach ($Property in $Object.PSObject.Properties.name) { $MemberTypeParams = @{ MemberType = 'NoteProperty' Name = $Property Value = ([SecureDatum]::GetObject($Object.$Property,$UnprotectParams)) } if ($MemberTypeParams.Value -is [scriptblock]) { $MemberTypeParams.MemberType = 'ScriptProperty' } $This | Add-Member @MemberTypeParams } } } [string] ToString() { return "{$($this.PSObject.Properties.Name -join ', ')}" } static [object] GetObject($object,$UnprotectParams) { if($null -eq $object) { return $null } elseif($object -is [PSCustomObject] -or $object -is [hashtable]) { return ([SecureDatum]::new($object,$UnprotectParams)) } elseif ($object -is [System.Collections.IEnumerable] -and $object -isnot [string]) { $collection = @() $collection = foreach ($item in $object) { [SecureDatum]::GetObject($item,$UnprotectParams) } return $collection } elseif($object -is [string] -and $object -match "^\[ENC=[\w\W]*\]$") { $UnprotectScriptBlock = " `$Base64Data = `"$object`" `[SecureDatum]::Unprotect(`$Base64Data.Trim(),`$this.UnprotectParams) " return ([scriptblock]::Create($UnprotectScriptBlock)) } else { return $object } } static [object] Unprotect($object,$UnprotectParams) { return (Unprotect-Datum -Base64Data $object @UnprotectParams) } } function ConvertTo-Datum { param ( [Parameter(ValueFromPipeline)] $InputObject, [AllowNull()] $DatumHandlers = @{} ) process { if ($null -eq $InputObject) { return $null } # if There's a matching filter, process associated command and return result if($HandlerNames = [string[]]$DatumHandlers.Keys) { foreach ($Handler in $HandlerNames) { $FilterModule,$FilterName = $Handler -split '::' if(!(Get-Module $FilterModule)) { Import-Module $FilterModule -force -ErrorAction Stop } $FilterCommand = Get-Command -ErrorAction SilentlyContinue ("{0}\Test-{1}Filter" -f $FilterModule,$FilterName) if($FilterCommand -and ($InputObject | &$FilterCommand)) { try { if($ActionCommand = Get-Command -ErrorAction SilentlyContinue ("{0}\Invoke-{1}Action" -f $FilterModule,$FilterName)) { $ActionParams = @{} $CommandOptions = $Datumhandlers.$handler.CommandOptions.Keys # Populate the Command's params with what's in the Datum.yml, or from variables foreach( $ParamName in $ActionCommand.Parameters.keys ) { if( $ParamName -in $CommandOptions ) { $ActionParams.add($ParamName,$Datumhandlers.$handler.CommandOptions[$ParamName]) } elseif($ValueInScope = Get-Variable -name $ParamName -ErrorAction SilentlyContinue -ValueOnly ){ $ActionParams.add($ParamName,$ValueInScope) } } return (&$ActionCommand @ActionParams) } } catch { Write-Warning "Error using Datum Handler $Handler, returning Input Object" $InputObject } } } } if ($InputObject -is [System.Collections.Hashtable] -or ($InputObject -is [System.Collections.Specialized.OrderedDictionary])) { $hashKeys = [string[]]$InputObject.Keys foreach ($Key in $hashKeys) { $InputObject[$Key] = ConvertTo-Datum -InputObject $InputObject[$Key] -DatumHandlers $DatumHandlers } $InputObject } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-Datum -InputObject $object -DatumHandlers $DatumHandlers } ) Write-Output -NoEnumerate $collection } elseif ($InputObject -is [psobject]) { $hash = [ordered]@{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertTo-Datum -InputObject $property.Value -DatumHandlers $DatumHandlers } $hash } else { $InputObject } } } function ConvertTo-ProtectedDatum {###########ConvertTo-DatumSecureObjectReader param ( [Parameter(ValueFromPipeline)] $InputObject, $UnprotectOptions ) process { if ($UnprotectOptions.contains('ClearTextPassword')) { $UnprotectOptions['password'] = $UnprotectOptions.ClearTextPassword | ConvertTo-SecureString -AsPlainText -force $null = $UnprotectOptions.remove('ClearTextPassword') } elseif ($UnprotectOptions.contains('SecureStringPassword')) { $UnprotectOptions['password'] = $UnprotectOptions.SecureStringPassword | ConvertTo-SecureString $null = $UnprotectOptions.remove('SecureStringPassword') } [SecureDatum]::GetObject($InputObject,$UnprotectOptions) } } function Get-MergeStrategyFromString { [CmdletBinding()] [OutputType([hashtable])] param( [String] $MergeStrategy ) switch -regex ($MergeStrategy) { '^First$|^MostSpecific$' { @{ strategy = 'MostSpecific' } } '^Unique$|^ArrayUniques$' { @{ strategy = 'Unique' } } '^hash$|^MergeTopKeys$' { @{ strategy = 'hash' options = @{ knockout_prefix = '--' sort_merged_arrays = $false merge_hash_arrays = $false } } } '^deep$|^MergeRecursively$' { @{ strategy = 'deep' options = @{ knockout_prefix = '--' sort_merged_arrays = $false merge_hash_arrays = $false } } } default { Write-Verbose "Couldn't Match the strategy $MergeStrategy" @{ strategy = 'MostSpecific' } } } } function Merge-Hashtable { [outputType([hashtable])] [cmdletBinding()] Param( [hashtable] $ReferenceHashtable, [hashtable] $DifferenceHashtable, [validateScript( { $_ -as [hashtable] -and $_.strategy -in @('hash','deep') -or $_ -in @('hash','deep') } )] $Strategy = @{ Strategy = 'deep' options = @{ knockoutprefix = '--' } }, $ChildStrategies = @{}, [string] $ParentPath ) $clonedReference = $ReferenceHashtable.Clone() if ($Strategy.options.knockout_prefix) { $KnockoutPrefix = $Strategy.options.knockout_prefix $KnockoutPrefixMatcher = [regex]::escape($KnockoutPrefix).insert(0,'^') Write-Debug "Knockout Prefix Matcher: $knockoutPrefixMatcher" } if($strategy -eq 'deep' -or $strategy.Strategy -eq 'deep') { $deepmerge = $true } else { $deepmerge = $false } $knockedOutKeys = $ReferenceHashtable.keys.where{$_ -match $KnockoutPrefixMatcher}.foreach{$_ -replace $KnockoutPrefixMatcher} Write-Debug "Knockout Keys: $($knockedOutKeys -join ', '); Ref Hashtable Keys $($ReferenceHashtable.keys -join ', ')" foreach ($currentKey in $DifferenceHashtable.keys) { Write-Debug "CurrentKey: $currentKey" if($currentKey -in $knockedOutKeys) { Write-Debug "`tThe Key $currentkey is knocked out from the reference Hashtable." } elseif ($currentKey -match $KnockoutPrefixMatcher -and !$ReferenceHashtable.contains(($currentKey -replace $KnockoutPrefixMatcher))) { # it's a knockout coming from a lower level key, it should only apply down from here Write-Debug "`tKnockout prefix found for $currentKey in Difference hashtable, and key not set in Reference hashtable" if(!$ReferenceHashtable.contains($currentKey)) { Write-Debug "`t..adding knockout prefixed key for $curretKey to block further merges" $clonedReference.add($currentKey,$null) } #Write-Warning "Removed key $($currentKey -replace $KnockoutPrefixMatcher) as we found the knockout prefix $KnockoutPrefix in the difference object" #$clonedReference.remove(($currentKey -replace $KnockoutPrefixMatcher)) } elseif (!$ReferenceHashtable.contains($currentKey) ) { #if the key does not exist in reference ht, create it using the DiffHt's value Write-Debug "`tAdded Key $currentKey using the DifferenceHashtable value: $($DifferenceHashtable[$currentKey]| Format-List * | out-String)" $clonedReference.add($currentKey,$DifferenceHashtable[$currentKey]) } else { #the key exists, and it's not a knockout entry if ($deepmerge -and ($ReferenceHashtable[$currentKey] -as [hashtable] -or ($ReferenceHashtable[$currentKey] -is [System.Collections.IEnumerable] -and $ReferenceHashtable[$currentKey] -isnot [string]))) { # both are hashtables and we're in Deepmerge mode Write-Debug "`t .. Merging Datums at current path $ParentPath\$CurrentKey" $subMerge = Merge-Datum -StartingPath (Join-Path $ParentPath $currentKey) -ReferenceDatum $ReferenceHashtable[$currentKey] -DifferenceDatum $DifferenceHashtable[$currentKey] -Strategies $ChildStrategies Write-Debug "# Submerge $($submerge|ConvertTo-Json)." $clonedReference[$currentKey] = $subMerge } ####################### ---> add array merge and hashtable[] merge here (hashtable[] merge based on defined subkey) else { #one is not an hashtable or we're not in deepmerge mode, leave the ClonedReference as-is Write-verbose "`tDeepmerge: $deepmerge; Ref[$currentkey] type $($ReferenceHashtable[$currentKey].GetType()); Diff[$currentkey] type $($DifferenceHashtable[$currentKey].GetType())" } } } return $clonedReference } <# $a = @{ keya = 1 keyb = 2 keyc = 3 '--keye' = $null } $b = @{ '--keya' = $null # removing keya keyb = 22 # won't override keyb keyd = 33 # will add keyd with value keye = 44 # keye should never be added, as it's removed from the ref ht } # simple merge: create keys from $b that do not exist in $a, remove --keys $d = [ordered]@{ a = [ordered]@{ x = 111 y = 222 z = 333 } b = 2 c = 3 d = 4 e = [ordered]@{ x = 111 '--y' = $null } } $c = @{ b = 0 #already defined, should ignore '--c' = $null #doesn't remove the key c from $c as it would violate the hierarchy #d missing intentionally, already defined e = @{ # key x omitted, already present y = 222 # this key 'y' should be added to $c.e z = 333 # this key 'z' should be added to $c.e } } $e = [ordered]@{ RootKey1 = [ordered]@{ subkey11 = [ordered]@{ subkey111 = 111 #'--Subkey112' = $null Subkey113 = 113 } subkey12 = [ordered]@{ subkey123 = 123 subkey124 = 124 } } RootKey2 = [ordered]@{ Subkey21 = [ordered]@{ Subkey211 = 211 Subkey212 = 212 Subkey213 = 213 } Subkey22 = @( 222 223 224 ) SubKey23 = @( [ordered]@{Name = 1; val1 = 1} [ordered]@{Name = 2; val1 = 2} [ordered]@{Name = 3; val1 = 3} ) } } $f = [ordered]@{ RootKey1 = [ordered]@{ subkey11 = [ordered]@{ subkey111 = 111 Subkey112 = 112 Subkey113 = 113 } subkey12 = [ordered]@{ subkey123 = 123 subkey124 = 124 } } RootKey2 = [ordered]@{ Subkey21 = [ordered]@{ Subkey211 = 2110 Subkey212 = 2120 Subkey213 = 2130 } Subkey22 = @( 221 ) SubKey23 = @( [ordered]@{Name = 1; val1 = 1} [ordered]@{Name = 2; val1 = 3} [ordered]@{Name = 3} ) } } $MergeParams = @{ StartingPath = 'root' ReferenceDatum = $e DifferenceDatum = $f Strategies = @{ 'root' = 'deep' 'root\rootkey2\Subkey22' = 'Unique' 'root\rootkey2\Subkey23' = 'Unique' '^.*' = 'deep' } } Merge-Datum @MergeParams #> function Get-FileProviderData { [CmdletBinding()] Param( $Path, [AllowNull()] $DatumHandlers = @{} ) Write-Verbose "Getting File Provider Data for Path: $Path" $File = Get-Item -Path $Path switch ($File.Extension) { '.psd1' { Import-PowerShellDataFile $File | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.json' { ConvertFrom-Json (Get-Content -Raw $Path) | ConvertTo-Datum -DatumHandlers $DatumHandlers } '.yml' { ConvertFrom-Yaml (Get-Content -raw $Path) -ordered | ConvertTo-Datum -DatumHandlers $DatumHandlers } Default { Get-Content -Raw $Path } } } function Get-MergeStrategyFromPath { [CmdletBinding()] Param( $Strategies, $PropertyPath ) Write-debug ">>> MergeStrategyFromPath $PropertyPath" # Select Relevant strategy # Use exact path match first # or try Regex in order if ($Strategies[$PropertyPath]) { $StrategyKey = $PropertyPath Write-debug "`tStrategy found for exact key $StrategyKey" } elseif($StrategyKey = [string]($Strategies.keys.where{$_.StartsWith('^') -and $_ -as [regex] -and $PropertyPath -match $_} | Select-Object -First 1)) { Write-debug "`tStrategy matching regex $StrategyKey" } else { Write-debug "`tNo Strategy found" return } Write-Debug "`tStrategyKey: $StrategyKey. $($Strategies[$StrategyKey].getType())" if( $Strategies[$StrategyKey] -is [string]) { Write-debug "`tReturning from String $($Strategies[$StrategyKey])" Get-MergeStrategyFromString $Strategies[$StrategyKey] } else { Write-Debug "`tReturning $($Strategies[$StrategyKey]|ConvertTo-Json)" $Strategies[$StrategyKey] } } function Invoke-ProtectedDatumAction { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','')] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string] $InputObject, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $PlainTextPassword, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) Write-Debug "Decrypting Datum using ProtectedData" $params = @{} foreach($ParamKey in $PSBoundParameters.keys) { if($ParamKey -in @('InputObject','PlainTextPassword')) { switch ($ParamKey) { 'PlainTextPassword' { $params.add('password',(ConvertTo-SecureString -AsPlainText -Force $PSBoundParameters[$ParamKey])) } 'InputObject' { $params.add('Base64Data',$InputObject) } } } else { $params.add($ParamKey,$PSBoundParameters[$ParamKey]) } } UnProtect-Datum @params } function Invoke-TestHandlerAction { Param( $Password, $test, $Datum ) @" Action: $handler Node: $($Node|FL *|Out-String) Params: $($PSBoundParameters | Convertto-Json) "@ } function Merge-Datum { [CmdletBinding()] param ( [string] $StartingPath, $ReferenceDatum, $DifferenceDatum, $Strategies = @{ '^.*' = 'MostSpecific' } ) Write-verbose "`r`n######## MERGE DATUM`r`nPATH: $StartingPath.`r`n`r`n" Write-Debug "`r`nStrategies : $($strategies|Convertto-Json)`r`n########" Write-Debug "REF $($ReferenceDatum|Convertto-JSon)" $Strategy = Get-MergeStrategyFromPath -Strategies $strategies -PropertyPath $startingPath -Verbose Write-Verbose "Strategy: $($Strategy | ConvertTo-Json)" # Merge with strategy $mergeParams = @{ ReferenceHashtable = $ReferenceDatum DifferenceHashtable = $DifferenceDatum Strategy = $Strategy ParentPath = $StartingPath } switch ($Strategy.Strategy) { 'MostSpecific' { return $ReferenceDatum} 'AllValues' { return $DifferenceDatum } 'Unique' { if($ReferenceDatum -as [hashtable]) { $ReferenceDatum = @($ReferenceDatum) } if($DifferenceDatum -as [hashtable]) { $DifferenceDatum = @($DifferenceDatum) } if($ReferenceDatum -as [hashtable[]]) { # it's an array of Hashtable objects, merge it by uniqueness # compare those with same set of Keys, then compare values? or compare object for sum of keys } elseif ($ReferenceDatum -is [System.Collections.IEnumerable] -and $ReferenceDatum -isnot [string]) { # it's another type of collection # cast refdatum to object[], add $diffDatum values, select unique, return @($ReferenceDatum + $DifferenceDatum) | Select-Object -Unique } } 'hash' { # ignore non-hashtable elements (replace with empty hash) if(!($ReferenceDatum -as [hashtable])) { $mergeParams['ReferenceHashtable'] = @{} } if(!($DifferenceDatum -as [hashtable])) { $mergeParams['DifferenceHashtable'] = @{} } # merge top layer keys, ignore subkeys Merge-Hashtable @mergeParams } 'deep' { if($ReferenceDatum -as [hashtable[]]) { Write-Debug "array of hashtable. Should merge by position, property or uniqueness" # it's an array of Hashtable, merge it by position, property, or uniqueness? } Write-Debug "adding Child Startegies: $($Strategies|ConvertTo-Json)" $mergeParams.Add('ChildStrategies',$Strategies) Merge-Hashtable @mergeParams } } # Strategy is MostSpecific --> No Merge # strategy is All Values --> No Merge, return all # Strategy is Unique --> cast to refdatum [object[]] + diffDatum | select Unique # Strategy is Hash --> Merge Keys. # Strategy is Deep # --> is Array or [object[]]Value # --> Merge Hash[]? # ---> No, only keep refDatum # ---> Uniques: cast to refdatum [object[]] + diffDatum | select Unique # ---> ByKey: Merge ArrayItem.Where{$_.key -match refArrayItem.Key} # --> SubMode? Deep or hash # ---> ByPosition: Merge Ref[itemIndex] with Diff[itemIndex] # --> SubMode? Deep or hash # --> is Hash/Ordered } function New-DatumFileProvider { Param( [alias('DataOptions')] [AllowNull()] $Store, [AllowNull()] $DatumHierarchyDefinition = @{}, $Path = $Store.StoreOptions.Path ) if (!$DatumHierarchyDefinition) { $DatumHierarchyDefinition = @{} } [FileProvider]::new($Path, $Store,$DatumHierarchyDefinition) } function New-DatumStructure { [CmdletBinding( DefaultParameterSetName = 'FromConfigFile' )] Param ( [Parameter( Mandatory, ParameterSetName = 'DatumHierarchyDefinition' )] [Alias('Structure')] [hashtable] $DatumHierarchyDefinition, [Parameter( Mandatory, ParameterSetName = 'FromConfigFile' )] [io.fileInfo] $DefinitionFile ) switch ($PSCmdlet.ParameterSetName) { 'DatumHierarchyDefinition' { if ($DatumHierarchyDefinition.contains('DatumStructure')) { Write-debug "Loading Datum from Parameter" } elseif($DatumHierarchyDefinition.Path) { $DatumHierarchyFolder = $DatumHierarchyDefinition.Path Write-Debug "Loading default Datum from given path $DatumHierarchyFolder" } else { Write-Warning "Desperate attempt to load Datum from Invocation origin..." $CallStack = Get-PSCallstack $DatumHierarchyFolder = $CallStack[-1].psscritroot Write-Warning " ---> $DatumHierarchyFolder" } } 'FromConfigFile' { if((Test-Path $DefinitionFile)) { $DefinitionFile = (Get-Item $DefinitionFile -ErrorAction Stop) Write-Debug "File $DefinitionFile found. Loading..." $DatumHierarchyDefinition = Get-FileProviderData $DefinitionFile.FullName if(!$DatumHierarchyDefinition.contains('ResolutionPrecedence')) { Throw 'Invalid Datum Hierarchy Definition' } $DatumHierarchyFolder = $DefinitionFile.directory.FullName Write-Debug "Datum Hierachy Parent folder: $DatumHierarchyFolder" } else { Throw "Datum Hierarchy Configuration not found" } } } $root = @{} if($DatumHierarchyFolder -and !$DatumHierarchyDefinition.DatumStructure) { $Structures = foreach ($Store in (Get-ChildItem -Directory -Path $DatumHierarchyFolder)) { @{ StoreName = $Store.BaseName StoreProvider = 'Datum::File' StoreOptions = @{ Path = $Store.FullName } } } if($DatumHierarchyDefinition.contains('DatumStructure')) { $DatumHierarchyDefinition['DatumStructure'] = $Structures } else { $DatumHierarchyDefinition.add('DatumStructure',$Structures) } } # Define the default hierachy to be the StoreNames, when nothing is specified if ($DatumHierarchyFolder -and !$DatumHierarchyDefinition.ResolutionPrecedence) { if($DatumHierarchyDefinition.contains('ResolutionPrecedence')) { $DatumHierarchyDefinition['ResolutionPrecedence'] = $Structures.StoreName } else { $DatumHierarchyDefinition.add('ResolutionPrecedence',$Structures.StoreName) } } # Adding the Datum Definition to Root object $root.add('__Definition',$DatumHierarchyDefinition) foreach ($store in $DatumHierarchyDefinition.DatumStructure){ $StoreParams = @{ Store = (ConvertTo-Datum $Store.clone()) Path = $store.StoreOptions.Path } # Accept Module Specification for Store Provider as String (unversioned) or Hashtable if($Store.StoreProvider -is [string]) { $StoreProviderModule, $StoreProviderName = $store.StoreProvider -split '::' } else { $StoreProviderModule = $Store.StoreProvider.ModuleName $StoreProviderName = $Store.StoreProvider.ProviderName if($Store.StoreProvider.ModuleVersion) { $StoreProviderModule = @{ ModuleName = $StoreProviderModule ModuleVersion = $Store.StoreProvider.ModuleVersion } } } if(!($Module = Get-Module $StoreProviderModule -ErrorAction SilentlyContinue)) { $Module = Import-Module $StoreProviderModule -Force -ErrorAction Stop -PassThru } $ModuleName = ($Module | Select-Object -First 1).Name $NewProvidercmd = Get-Command ("{0}\New-Datum{1}Provider" -f $ModuleName, $StoreProviderName) if( $StoreParams.Path -and ![io.path]::IsPathRooted($StoreParams.Path) -and $DatumHierarchyFolder ) { Write-Debug "Replacing Store Path with AbsolutePath" $StorePath = Join-Path $DatumHierarchyFolder $StoreParams.Path -Resolve -ErrorAction Stop $StoreParams['Path'] = $StorePath } if ($NewProvidercmd.Parameters.keys -contains 'DatumHierarchyDefinition') { Write-Debug "Adding DatumHierarchyDefinition to Store Params" $StoreParams.add('DatumHierarchyDefinition',$DatumHierarchyDefinition) } $storeObject = &$NewProvidercmd @StoreParams Write-Debug "Adding key $($store.storeName) to Datum root object" $root.Add($store.StoreName,$storeObject) } #return the Root Datum hashtable $root } #Requires -Modules ProtectedData function Protect-Datum { [CmdletBinding()] [OutputType([PSObject])] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [PSObject] $InputObject, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [System.Security.SecureString] $Password, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Int] $MaxLineLength = 100, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) begin { } process { Write-Verbose "Deserializing the Object from Base64" $ProtectDataParams = @{ InputObject = $InputObject } Write-verbose "Calling Protect-Data $($PSCmdlet.ParameterSetName)" Switch ($PSCmdlet.ParameterSetName) { 'ByCertificae' { $ProtectDataParams.Add('Certificate',$Certificate)} 'ByPassword' { $ProtectDataParams.Add('Password',$Password) } } $securedData = Protect-Data @ProtectDataParams $xml = [System.Management.Automation.PSSerializer]::Serialize($securedData, 5) $bytes = [System.Text.Encoding]::UTF8.GetBytes($xml) $Base64String = [System.Convert]::ToBase64String($bytes) if($MaxLineLength -gt 0) { $Base64DataBlock = [regex]::Replace($Base64String,"(.{$MaxLineLength})","`$1`r`n") } else { $Base64DataBlock = $Base64String } if(!$NoEncapsulation) { $Header,$Base64DataBlock,$Footer -join '' } else { $Base64DataBlock } } } Function Resolve-Datum { [cmdletBinding()] Param( [Parameter( Mandatory )] [string] $PropertyPath, [Parameter( Position = 1 )] [Alias('Node')] $Variable = $ExecutionContext.InvokeCommand.InvokeScript('$Node'), [string] $VariableName = 'Node', [Alias('DatumStructure')] $DatumTree = $ExecutionContext.InvokeCommand.InvokeScript('$ConfigurationData.Datum'), [Parameter( ParameterSetName = 'UseMergeOptions' )] [Alias('SearchBehavior')] $options, [string[]] [Alias('SearchPaths')] $PathPrefixes = $DatumTree.__Definition.ResolutionPrecedence, [int] $MaxDepth = $( if($MxdDpth = $DatumTree.__Definition.default_lookup_options.MaxDepth) { $MxdDpth } else { -1 }) ) # Manage lookup options: <# default_lookup_options Lookup_options options (argument) Behaviour Absent Absent Absent MostSpecific for ^.* Present Absent Absent default_lookup_options + most Specific if not ^.* Absent Present Absent lookup_options + Default to most Specific if not ^.* Absent Absent Present options + Default to Most Specific if not ^.* Present Present Absent Lookup_options + Default for ^.* if !Exists Present Absent Present options + Default for ^.* if !Exists Absent Present Present options override lookup options + Most Specific if !Exists Present Present Present options override lookup options + default for ^.* +========================+================+====================+============================================================+ | default_lookup_options | Lookup_options | options (argument) | Behaviour | +========================+================+====================+============================================================+ | Absent | Absent | Absent | MostSpecific for ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Absent | Absent | default_lookup_options + most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Absent | Present | Absent | lookup_options + Default to most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Absent | Absent | Present | options + Default to Most Specific if not ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Present | Absent | Lookup_options + Default for ^.* if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Absent | Present | options + Default for ^.* if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Absent | Present | Present | options override lookup options + Most Specific if !Exists | +------------------------+----------------+--------------------+------------------------------------------------------------+ | Present | Present | Present | options override lookup options + default for ^.* | +------------------------+----------------+--------------------+------------------------------------------------------------+ #> # https://docs.puppet.com/puppet/5.0/hiera_merging.html # Configure Merge Behaviour in the Datum structure (as per Puppet hiera) if( !$DatumTree.__Definition.default_lookup_options ) { $default_options = [ordered]@{ '^.*' = @{ strategy = 'MostSpecific' } } Write-Verbose "Default option not found in Datum Tree" } else { if($DatumTree.__Definition.default_lookup_options -is [string]) { $default_options = $(Get-MergeStrategyFromString -MergeStrategy $DatumTree.__Definition.default_lookup_options) } else { $default_options = $DatumTree.__Definition.default_lookup_options } Write-Verbose "Found default options in Datum Tree of type $($default_options.Strategy)." } if( $lookup_options = $DatumTree.__Definition.lookup_options) { Write-Debug "Lookup options found." } else { $lookup_options = @{} } # Transform options from string to strategy hashtable foreach ($optKey in ([string[]]$lookup_options.keys)) { if($lookup_options[$optKey] -is [string]) { $lookup_options[$optKey] = Get-MergeStrategyFromString -MergeStrategy $lookup_options[$optKey] } } foreach ($optKey in ([string[]]$options.keys)) { if($options[$optKey] -is [string]) { $options[$optKey] = Get-MergeStrategyFromString -MergeStrategy $options[$optKey] } } # using options if specified or lookup_options otherwise if (!$options) { $options = $lookup_options } # Add default strategy for ^.* if not present if(([string[]]$Options.keys) -notcontains '^.*') { $options.add('^.*',$default_options) } # Create the variable to be used as Pivot in prefix path if( $Variable -and $VariableName ) { Set-Variable -Name $VariableName -Value $Variable -Force } # Scriptblock in path detection patterns $Pattern = '(?<opening><%=)(?<sb>.*?)(?<closure>%>)' $PropertySeparator = [IO.Path]::DirectorySeparatorChar $splitPattern = [regex]::Escape($PropertySeparator) $Depth = 0 $MergeResult = $null # Get the strategy for this path, to be used for merging $StartingMergeStrategy = Get-MergeStrategyFromPath -PropertyPath $PropertyPath -Strategies $options # Walk every search path in listed order, and return datum when found at end of path foreach ($SearchPrefix in $PathPrefixes) { #through the hierarchy $ArraySb = [System.Collections.ArrayList]@() $CurrentSearch = Join-Path $SearchPrefix $PropertyPath Write-Verbose '' Write-Verbose "Searching: $CurrentSearch" #extract script block for execution into array, replace by substition strings {0},{1}... $newSearch = [regex]::Replace($CurrentSearch, $Pattern, { param($match) $expr = $match.groups['sb'].value $index = $ArraySb.Add($expr) "`$({$index})" }, @('IgnoreCase', 'SingleLine', 'MultiLine')) $PathStack = $newSearch -split $splitPattern # Get value for this property path $DatumFound = Resolve-DatumPath -Node $Node -DatumTree $DatumTree -PathStack $PathStack -PathVariables $ArraySb Write-Debug "Depth: $depth; Merge Behavior: $($options|Convertto-Json|Out-String)" #Stop processing further path at first value in 'MostSpecific' mode (called 'first' in Puppet hiera) if ($DatumFound -and ($StartingMergeStrategy.Strategy -eq 'MostSpecific')) { return $DatumFound } elseif ( $DatumFound ) { if(!$MergeResult) { $MergeResult = $DatumFound } else { $MergeParams = @{ StartingPath = $PropertyPath ReferenceDatum = $MergeResult DifferenceDatum = $DatumFound Strategies = $options } $MergeResult = Merge-Datum @MergeParams } } #if we've reached the Maximum Depth allowed, return current result and stop further execution if ($Depth -eq $MaxDepth) { Write-Debug "Max depth of $MaxDepth reached. Stopping." return $MergeResult } } $MergeResult } function Resolve-DatumPath { [CmdletBinding()] param( [Alias('Variable')] $Node, [Alias('DatumStructure')] $DatumTree, [string[]] $PathStack, [System.Collections.ArrayList] $PathVariables ) $currentNode = $DatumTree $PropertySeparator = '.' #[io.path]::DirectorySeparatorChar $index = -1 Write-Debug "`t`t`t" foreach ($StackItem in $PathStack) { $index++ $RelativePath = $PathStack[0..$index] Write-Debug "`t`t`tCurrent Path: `$Datum$PropertySeparator$($RelativePath -join $PropertySeparator)" $RemainingStack = $PathStack[$index..($PathStack.Count-1)] Write-Debug "`t`t`t`tbranch of path Left to walk: $PropertySeparator$($RemainingStack[1..$RemainingStack.Length] -join $PropertySeparator)" if ( $StackItem -match '\{\d+\}') { Write-Debug -Message "`t`t`t`t`tReplacing expression $StackItem" $StackItem = [scriptblock]::Create( ($StackItem -f ([string[]]$PathVariables)) ).Invoke() Write-Debug -Message ($StackItem | Format-List * | Out-String) $PathItem = $stackItem } else { $PathItem = $CurrentNode.($ExecutionContext.InvokeCommand.ExpandString($StackItem)) } # if $PathItem is $null, it won't have subkeys, stop execution for this Prefix if($null -eq $PathItem) { Write-Verbose -Message " NULL FOUND at `$Datum.$($ExecutionContext.InvokeCommand.ExpandString(($RelativePath -join $PropertySeparator) -f [string[]]$PathVariables))`t`t <`$Datum$PropertySeparator$(($RelativePath -join $PropertySeparator) -f [string[]]$PathVariables)>" if($RemainingStack.Count -gt 1) { Write-Verbose -Message "`t`t----> before: $propertySeparator$($ExecutionContext.InvokeCommand.ExpandString(($RemainingStack[1..($RemainingStack.Count-1)] -join $PropertySeparator)))`t`t <$(($RemainingStack[1..($RemainingStack.Count-1)] -join $PropertySeparator) -f [string[]]$PathVariables)>" } Return $null } else { $CurrentNode = $PathItem } if ($RemainingStack.Count -eq 1) { Write-Verbose -Message " VALUE found at `$Datum$PropertySeparator$($ExecutionContext.InvokeCommand.ExpandString(($RelativePath -join $PropertySeparator) -f [string[]]$PathVariables))" Write-Output $CurrentNode } } } function Test-ProtectedDatumFilter { Param( [Parameter( ValueFromPipeline )] $InputObject ) $InputObject -is [string] -and $InputObject.Trim() -match "^\[ENC=[\w\W]*\]$" } function Test-TestHandlerFilter { Param( [Parameter( ValueFromPipeline )] $inputObject ) $InputObject -is [string] -and $InputObject -match "^\[TEST=[\w\W]*\]$" } #Requires -Modules ProtectedData function Unprotect-Datum { [CmdletBinding()] [OutputType([PSObject])] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string] $Base64Data, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [System.Security.SecureString] $Password, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) begin { } process { if (!$NoEncapsulation) { Write-Verbose "Removing $header DATA $footer " $Base64Data = $Base64Data -replace "^$([regex]::Escape($Header))" -replace "$([regex]::Escape($Footer))$" } Write-Verbose "Deserializing the Object from Base64" $bytes = [System.Convert]::FromBase64String($Base64Data) $xml = [System.Text.Encoding]::UTF8.GetString($bytes) $obj = [System.Management.Automation.PSSerializer]::Deserialize($xml) $UnprotectDataParams = @{ InputObject = $obj } Write-verbose "Calling Unprotect-Data $($PSCmdlet.ParameterSetName)" Switch ($PSCmdlet.ParameterSetName) { 'ByCertificae' { $UnprotectDataParams.Add('Certificate',$Certificate)} 'ByPassword' { $UnprotectDataParams.Add('Password',$Password) } } Unprotect-Data @UnprotectDataParams } } |