Private/Transforms.ps1
|
# Catalog transforms and the helpers that place a value at a property path. # # idOf:<type> a name as that type's id, through POST /deployments/resources/read # idsOf:<type> names as ids # relationOf:<kind> names as [{id, type: <kind>}], resolved through VirtualMetric/<kind>s # secure a SecureString, kept as one until the body is serialized # merge a hashtable deep-merged into the object at the target # status enable/disable: properties.status active or passive function Resolve-VMetricResourceId { # The ids of resources of one type, by name, in the order given. A name nothing answers to is an error. [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory)] [string] $Type, [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $Name, [AllowNull()] [System.Collections.IDictionary] $Parent ) if ($Name.Count -eq 0) { return , ([string[]]@()) } $resources = foreach ($item in $Name) { $lookup = [ordered]@{ type = $Type; name = $item } if ($Parent) { $lookup['parent'] = $Parent } $lookup } $answer = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($resources) }) $found = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $answer -Name 'resources') $ids = [System.Collections.Generic.List[string]]::new() for ($i = 0; $i -lt $Name.Count; $i++) { $match = $null foreach ($candidate in $found) { if ([string](Get-VMetricMember -InputObject $candidate -Name 'name') -ceq $Name[$i]) { $match = $candidate break } } if (-not $match -and $i -lt $found.Count) { $match = $found[$i] } if (-not $match -or -not (Get-VMetricMember -InputObject $match -Name 'exists')) { throw (New-VMetricException -Message "No $Type named '$(ConvertTo-VMetricSafeText -Text $Name[$i])' was found." -Code 'ResourceNotFound' -Category ObjectNotFound) } $ids.Add([string](Get-VMetricMember -InputObject $match -Name 'id')) } return , $ids.ToArray() } function Resolve-VMetricParamValue { # A bound value as the catalog param's transform makes it. Switches become booleans. [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Param, [AllowNull()] [object] $Value ) if ($Value -is [System.Management.Automation.SwitchParameter]) { $Value = $Value.IsPresent } $transform = [string](Get-VMetricMember -InputObject $Param -Name 'transform') if (-not $transform -or $transform -eq 'secure' -or $transform -eq 'merge') { if ($Value -is [array]) { return , $Value } return $Value } if ($transform -eq 'status') { if ($Value -is [bool]) { return $(if ($Value) { 'active' } else { 'passive' }) } return [string]$Value } $separator = $transform.IndexOf(':') $name = if ($separator -gt 0) { $transform.Substring(0, $separator) } else { $transform } $argument = if ($separator -gt 0) { $transform.Substring($separator + 1) } else { '' } if ($name -eq 'idOf') { if ($null -eq $Value -or [string]$Value -eq '') { return '' } return (Resolve-VMetricResourceId -Type $argument -Name @([string]$Value))[0] } if ($name -eq 'idsOf' -or $name -eq 'relationOf') { $names = [System.Collections.Generic.List[string]]::new() foreach ($item in (ConvertTo-VMetricArray -InputObject $Value)) { if ([string]$item -ne '') { $names.Add([string]$item) } } if ($name -eq 'idsOf') { return , (Resolve-VMetricResourceId -Type $argument -Name $names.ToArray()) } $relations = [System.Collections.Generic.List[object]]::new() foreach ($id in (Resolve-VMetricResourceId -Type "VirtualMetric/$($argument)s" -Name $names.ToArray())) { $relations.Add([ordered]@{ id = $id; type = $argument }) } return , $relations.ToArray() } throw (New-VMetricException -Message "The cmdlet catalog uses a transform this module does not know ($transform). Update the VirtualMetric module." ` -Code 'UnknownTransform' -Category NotImplemented -Terminating) } function Get-VMetricEntryList { # The name/value pairs of a dictionary or a PSCustomObject. [CmdletBinding()] [OutputType([object[]])] param( [AllowNull()] [object] $InputObject ) $entries = [System.Collections.Generic.List[object]]::new() if ($InputObject -is [System.Collections.IDictionary]) { foreach ($key in $InputObject.Keys) { $entries.Add([pscustomobject]@{ Name = [string]$key; Value = $InputObject[$key] }) } } elseif (Test-VMetricObjectLike -InputObject $InputObject) { foreach ($property in $InputObject.psobject.Properties) { if ($property.MemberType -eq 'NoteProperty') { $entries.Add([pscustomobject]@{ Name = $property.Name; Value = $property.Value }) } } } return , $entries.ToArray() } function Get-VMetricChildDictionary { # The dictionary under a key, created (or converted from a PSCustomObject) when needed. The existing key's # spelling is kept: the engine reads property names case-sensitively. [CmdletBinding()] [OutputType([System.Collections.IDictionary])] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Parent, [Parameter(Mandatory)] [string] $Name ) $key = Get-VMetricDictionaryKey -Dictionary $Parent -Name $Name if ($null -eq $key) { $key = $Name $Parent[$key] = [ordered]@{} } elseif ($Parent[$key] -isnot [System.Collections.IDictionary]) { if (Test-VMetricObjectLike -InputObject $Parent[$key]) { $Parent[$key] = Copy-VMetricData -InputObject $Parent[$key] } else { $Parent[$key] = [ordered]@{} } } return $Parent[$key] } function Set-VMetricPathValue { # Sets the value at a dotted path (properties.properties.port), creating the objects on the way. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Changes an in-memory object only.')] [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path, [AllowNull()] [object] $Value ) $segments = $Path.Split('.') $node = $Target for ($i = 0; $i -lt $segments.Length - 1; $i++) { $node = Get-VMetricChildDictionary -Parent $node -Name $segments[$i] } $last = $segments[$segments.Length - 1] $key = Get-VMetricDictionaryKey -Dictionary $node -Name $last if ($null -eq $key) { $key = $last } $node[$key] = $Value } function Get-VMetricPathValue { # The value at a dotted path, matching keys ignoring case; $null when a step is missing. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path ) $node = $Target foreach ($segment in $Path.Split('.')) { $node = Get-VMetricMember -InputObject $node -Name $segment if ($null -eq $node) { return $null } } if ($node -is [array]) { return , $node } return $node } function Set-VMetricRelationValue { # relationOf: the relations of its own kind (director or cluster) are replaced and the others kept, so # -Director and -Cluster combine, and Set -Director keeps a device's clusters. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Changes an in-memory object only.')] [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [string] $Kind, [AllowNull()] [object] $Value ) $relations = [System.Collections.Generic.List[object]]::new() foreach ($relation in (ConvertTo-VMetricArray -InputObject (Get-VMetricPathValue -Target $Target -Path $Path))) { if ([string](Get-VMetricMember -InputObject $relation -Name 'type') -ne $Kind) { $relations.Add($relation) } } foreach ($relation in (ConvertTo-VMetricArray -InputObject $Value)) { $relations.Add($relation) } Set-VMetricPathValue -Target $Target -Path $Path -Value $relations.ToArray() } function Merge-VMetricDictionary { # Deep-merges Source into Target: objects merge key by key, anything else (a list included) replaces. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [object] $Source ) foreach ($entry in (Get-VMetricEntryList -InputObject $Source)) { $key = Get-VMetricDictionaryKey -Dictionary $Target -Name $entry.Name if ($null -ne $key -and (Test-VMetricObjectLike -InputObject $entry.Value) -and (Test-VMetricObjectLike -InputObject $Target[$key])) { $child = Get-VMetricChildDictionary -Parent $Target -Name $key Merge-VMetricDictionary -Target $child -Source $entry.Value continue } if ($null -eq $key) { $key = $entry.Name } $Target[$key] = Copy-VMetricData -InputObject $entry.Value } } function Merge-VMetricPathValue { # The merge transform: deep-merges a hashtable into the object at a dotted path. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.IDictionary] $Target, [Parameter(Mandatory)] [string] $Path, [AllowNull()] [object] $Value, [string] $ParameterName = 'Property' ) if ($null -eq $Value) { return } if (-not (Test-VMetricObjectLike -InputObject $Value)) { throw (New-VMetricException -Message "-$ParameterName takes a hashtable of the properties to change, such as @{ name = 'value' }." ` -Code 'InvalidMergeValue' -Category InvalidArgument) } $node = $Target foreach ($segment in $Path.Split('.')) { $node = Get-VMetricChildDictionary -Parent $node -Name $segment } Merge-VMetricDictionary -Target $node -Source $Value } function Get-VMetricSecretText { # The text of every SecureString in a value, whatever its length: what a preview must never print, whatever # the server echoes. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [object] $InputObject ) $found = [System.Collections.Generic.List[string]]::new() $walk = { param($Value) if ($null -eq $Value -or $Value -is [string] -or $Value -is [System.ValueType]) { return } if ($Value -is [System.Security.SecureString]) { $text = [System.Net.NetworkCredential]::new('', $Value).Password if ($text.Length -gt 0) { $found.Add($text) } return } foreach ($entry in (Get-VMetricEntryList -InputObject $Value)) { & $walk $entry.Value } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [System.Collections.IDictionary]) { foreach ($item in $Value) { & $walk $item } } } & $walk $InputObject return , $found.ToArray() } function Hide-VMetricSecretInObject { # Masks, in place, every string of an emitted object (a server's diagnostic, a what-if delta) that carries # the text of a secret the request held. [CmdletBinding()] param( [AllowNull()] [object] $InputObject, [AllowNull()] [string[]] $Secret ) if (-not $Secret -or $null -eq $InputObject) { return } $walk = { param($Value) if ($Value -is [System.Collections.IList]) { for ($i = 0; $i -lt $Value.Count; $i++) { if ($Value[$i] -is [string]) { $Value[$i] = Hide-VMetricSecretText -Text $Value[$i] -Secret $Secret } else { & $walk $Value[$i] } } return } if (-not (Test-VMetricObjectLike -InputObject $Value)) { return } if ($Value -is [System.Collections.IDictionary]) { foreach ($key in @($Value.Keys)) { if ($Value[$key] -is [string]) { $Value[$key] = Hide-VMetricSecretText -Text $Value[$key] -Secret $Secret } else { & $walk $Value[$key] } } return } foreach ($property in $Value.psobject.Properties) { if ($property.MemberType -ne 'NoteProperty') { continue } if ($property.Value -is [string]) { $property.Value = Hide-VMetricSecretText -Text $property.Value -Secret $Secret } else { & $walk $property.Value } } } & $walk $InputObject } function ConvertTo-VMetricJsonStringText { # A string as it reads between the quotes of a JSON string: as ConvertTo-Json writes it, or with -Html as # Go's encoding/json does (<, > and & escaped too), which is how the API echoes one inside JSON text. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [AllowEmptyString()] [string] $Text, [switch] $Html ) $escape = '\' + 'u' $builder = [System.Text.StringBuilder]::new() foreach ($character in $Text.ToCharArray()) { $code = [int]$character switch ($code) { 0x22 { [void]$builder.Append('\"') } 0x5C { [void]$builder.Append('\\') } 0x0A { [void]$builder.Append('\n') } 0x0D { [void]$builder.Append('\r') } 0x09 { [void]$builder.Append('\t') } 0x08 { [void]$builder.Append('\b') } 0x0C { [void]$builder.Append('\f') } default { if ($code -lt 0x20 -or ($Html -and ($code -in @(0x3C, 0x3E, 0x26, 0x2028, 0x2029)))) { [void]$builder.Append($escape).Append($code.ToString('x4')) } else { [void]$builder.Append($character) } } } } $builder.ToString() } function Get-VMetricSecretSpelling { # Every spelling of the secrets a message may carry: as they are, and as they read inside JSON text (a # secret holding a quote or a backslash is echoed as \" or \\), longest first. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [string[]] $Secret ) $spellings = [System.Collections.Generic.List[string]]::new() foreach ($value in $Secret) { if (-not $value) { continue } foreach ($spelling in @($value, (ConvertTo-VMetricJsonStringText -Text $value), (ConvertTo-VMetricJsonStringText -Text $value -Html))) { if (-not $spellings.Contains($spelling)) { $spellings.Add($spelling) } } } return , @($spellings | Sort-Object -Property Length -Descending) } function Hide-VMetricSecretText { # Masks the secrets in a message: a text that is a secret, of any length, becomes ***; a secret of three # characters or more is replaced wherever the text holds it, as it is or as JSON escapes it. (A shorter one # would mask every word it happens to spell; a value that holds one is masked where it sits, by # Hide-VMetricSecretValue.) [CmdletBinding()] [OutputType([string])] param( [AllowNull()] [AllowEmptyString()] [string] $Text, [AllowNull()] [string[]] $Secret ) if ([string]::IsNullOrEmpty($Text) -or -not $Secret) { return $Text } $spellings = Get-VMetricSecretSpelling -Secret $Secret foreach ($spelling in $spellings) { if ([string]::Equals($Text, $spelling, [System.StringComparison]::Ordinal)) { return '***' } } foreach ($spelling in $spellings) { if ($spelling.Length -ge 3) { $Text = $Text.Replace($spelling, '***') } } return $Text } function Get-VMetricSecretPath { # Where a value holds a SecureString, as the engine writes a what-if delta's path: properties.value, # properties.headers[0].value. A resource's secret paths are masked in its preview whatever the server # answers there. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [object] $InputObject ) $paths = [System.Collections.Generic.List[string]]::new() $walk = { param($Value, [string] $Path) if ($null -eq $Value -or $Value -is [string] -or $Value -is [System.ValueType]) { return } if ($Value -is [System.Security.SecureString]) { $paths.Add($Path) return } if ($Value -is [System.Collections.IDictionary] -or (Test-VMetricObjectLike -InputObject $Value)) { foreach ($entry in (Get-VMetricEntryList -InputObject $Value)) { & $walk $entry.Value $(if ($Path) { "$Path.$($entry.Name)" } else { $entry.Name }) } return } if ($Value -is [System.Collections.IEnumerable]) { $index = 0 foreach ($item in $Value) { & $walk $item "$Path[$index]" $index++ } } } & $walk $InputObject '' return , $paths.ToArray() } function Hide-VMetricSecretValue { # A copy of a what-if value for display, masked before it is encoded: a value at one of SecretPath (the # request's secrets' own paths) is *** whatever it holds, and every other string goes through # Hide-VMetricSecretText. Masking the encoded text instead would miss a secret JSON escapes, or a short one. [CmdletBinding()] param( [AllowNull()] [object] $InputObject, [AllowNull()] [string[]] $Secret, # The value's own path (properties.value). [AllowNull()] [AllowEmptyString()] [string] $Path, [AllowNull()] [string[]] $SecretPath ) $secrets = @($Secret) $secretPaths = @($SecretPath) $walk = { param($Value, [string] $Here) if ($null -eq $Value) { return $null } $atSecret = $false foreach ($candidate in $secretPaths) { if ($candidate -and [string]::Equals($candidate, $Here, [System.StringComparison]::OrdinalIgnoreCase)) { $atSecret = $true } } if ($Value -is [System.Security.SecureString]) { return '***' } if ($Value -is [string]) { if ($atSecret -and $Value -ne '') { return '***' } return (Hide-VMetricSecretText -Text $Value -Secret $secrets) } if ($Value -is [System.ValueType]) { if ($atSecret) { return '***' } return $Value } if ($Value -is [System.Collections.IDictionary] -or (Test-VMetricObjectLike -InputObject $Value)) { $map = [ordered]@{} foreach ($entry in (Get-VMetricEntryList -InputObject $Value)) { $map[$entry.Name] = & $walk $entry.Value $(if ($Here) { "$Here.$($entry.Name)" } else { $entry.Name }) } return $map } if ($Value -is [System.Collections.IEnumerable]) { $list = [System.Collections.Generic.List[object]]::new() $index = 0 foreach ($item in $Value) { $list.Add((& $walk $item "$Here[$index]")) $index++ } return , $list.ToArray() } return $Value } $masked = & $walk $InputObject ([string]$Path) if ($masked -is [array]) { return , $masked } return $masked } |