Private/CatalogCmdlet.ps1
|
# Invoke-VMetricCatalogCmdlet: the body of every generated cmdlet. It implements the catalog's generic # semantics (contract section 3.2), which Cloud Shell implements identically: # # get item with the id (the ById set), else the list, page by page (itemCount=100) until total; an # exactName filter keeps only items whose name equals the value, ignoring case. # new properties from the params' targets and transforms, deployed through the engine as a one-resource # deployment (POST /deployments/resources?wait=true), then read back through the get cmdlet. # set the current template form (POST /deployments/resources/read), the params applied onto a copy, # deployed the same way. # enable, disable # set with properties.status active or passive. # remove the id (-Id, or -Name through resolveIdBy), ShouldProcess (ConfirmImpact High), then DELETE. # action a REST call with no engine involved, with ShouldProcess when it changes state. # # -WhatIf and -Confirm on new, set, enable and disable show the engine's own preview # (POST /deployments/resources/preview): what it would actually change. function Test-VMetricBound { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [string] $Name ) foreach ($key in $Bound.Keys) { if ([string]$key -eq $Name) { return $true } } return $false } function Get-VMetricPathPlaceholder { # The {names} in a path template, in order. [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [AllowEmptyString()] [string] $Template ) $names = [System.Collections.Generic.List[string]]::new() if ($Template) { foreach ($match in [regex]::Matches($Template, '\{([^}]+)\}')) { $names.Add($match.Groups[1].Value) } } return , $names.ToArray() } function ConvertTo-VMetricPathSegment { # A value as one segment of a request path, URL-encoded. EscapeDataString leaves '.' and '..' as they are, # and .NET then collapses them (/pipelines/5/children/.. is sent as /pipelines/5/), so a request aimed at # a child would reach its parent: those two, and an empty value, are refused before any request is made. [CmdletBinding()] [OutputType([string])] param( [AllowNull()] [object] $Value, # What the value is, for the error: -Id, {childId}. [Parameter(Mandatory)] [string] $Name ) $text = [string]$Value if ($text.Trim() -in @('', '.', '..')) { $shown = ConvertTo-VMetricSafeText -Text $text throw (New-VMetricException -Message "$Name cannot be '$shown': a value that names a resource in the request path may not be empty, '.' or '..'." ` -Code 'InvalidPathValue' -Category InvalidArgument) } [System.Uri]::EscapeDataString($text) } function Resolve-VMetricPathTemplate { # A path template with its placeholders filled, each value one URL-encoded segment # (ConvertTo-VMetricPathSegment); $null when one has no value. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Template, [Parameter(Mandatory)] [AllowEmptyCollection()] [hashtable] $Values ) $builder = [System.Text.StringBuilder]::new() $position = 0 foreach ($match in [regex]::Matches($Template, '\{([^}]+)\}')) { [void]$builder.Append($Template.Substring($position, $match.Index - $position)) $name = $match.Groups[1].Value $value = $null if ($Values.ContainsKey($name)) { $value = $Values[$name] } if ($null -eq $value -or [string]$value -eq '') { return $null } [void]$builder.Append((ConvertTo-VMetricPathSegment -Value $value -Name "{$name}")) $position = $match.Index + $match.Length } [void]$builder.Append($Template.Substring($position)) $builder.ToString() } function Resolve-VMetricRestArgument { # The bound params of a REST kind, each through its transform, by where it goes: path values by key, the # query, and the body (a dotted key nests). [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [object] $Entry, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound ) $pathValues = @{} $query = [ordered]@{} $body = [ordered]@{} $params = Get-VMetricCatalogParameter -Entry $Entry # A path value given as it is sent is checked before anything is sent (a transform's name is looked up # first; the id it resolves to is checked where the path is built). foreach ($param in $params) { $name = [string](Get-VMetricMember -InputObject $param -Name 'name') if ([string](Get-VMetricMember -InputObject $param -Name 'in') -eq 'path' -and -not (Get-VMetricMember -InputObject $param -Name 'transform') -and (Test-VMetricBound -Bound $Bound -Name $name)) { $null = ConvertTo-VMetricPathSegment -Value $Bound[$name] -Name "-$name" } } foreach ($param in $params) { $name = [string](Get-VMetricMember -InputObject $param -Name 'name') $location = [string](Get-VMetricMember -InputObject $param -Name 'in') if (-not $location -or -not (Test-VMetricBound -Bound $Bound -Name $name)) { continue } $key = [string](Get-VMetricMember -InputObject $param -Name 'key') if (-not $key) { $key = $name } $value = Resolve-VMetricParamValue -Param $param -Value $Bound[$name] if ($location -eq 'path') { $pathValues[$key] = $value } elseif ($location -eq 'query') { $query[$key] = $value } elseif ($location -eq 'body') { Set-VMetricPathValue -Target $body -Path $key -Value $value } } [pscustomobject]@{ Path = $pathValues Query = $query Body = $body } } function Get-VMetricTypeField { # A catalog type's idField or nameField, with its default. [CmdletBinding()] [OutputType([string])] param( [AllowNull()] [AllowEmptyString()] [string] $TypeName, [Parameter(Mandatory)] [ValidateSet('idField', 'nameField')] [string] $Field ) $type = Get-VMetricCatalogType -Name $TypeName $value = [string](Get-VMetricMember -InputObject $type -Name $Field) if ($value) { return $value } if ($Field -eq 'idField') { return 'id' } return 'name' } function Invoke-VMetricCatalogList { # Every item of a get cmdlet's list, page by page: itemCount=100 and pageNumber from 1 until total is # reached (or a short or empty page, or a page of items already seen when the endpoint ignores paging). # -NameFilter keeps only items whose name equals it, ignoring case. # # Every page is read before the first item is written. The cmdlet the items are piped to may change the # list as it runs (Get-VMetricDevice | Remove-VMetricDevice deletes; Get-VMetricDevice -Status passive | # Enable-VMetricDevice moves each item out of the filter), and a page read after that would start past items # nobody read: 250 matching, 150 removed. What list.expand reads per item is read as the item is written, # so Select-Object -First 1 still reads one. [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Entry, [hashtable] $PathValues = @{}, [System.Collections.IDictionary] $Query, [AllowNull()] [AllowEmptyString()] [string] $NameFilter, # Raw API items instead of typed objects. [switch] $Raw, # The public cmdlet, whose warning stream says an item was skipped. [System.Management.Automation.PSCmdlet] $Cmdlet ) $list = Get-VMetricMember -InputObject $Entry -Name 'list' $template = [string](Get-VMetricMember -InputObject $list -Name 'path') $path = Resolve-VMetricPathTemplate -Template $template -Values $PathValues if ($null -eq $path) { $needed = (Get-VMetricPathPlaceholder -Template $template) -join ', ' throw (New-VMetricException -Message "$($Entry.name) needs a value for: $needed." -Code 'MissingParameter' -Category InvalidArgument) } $method = [string](Get-VMetricMember -InputObject $list -Name 'method') if (-not $method) { $method = 'GET' } $itemsField = [string](Get-VMetricMember -InputObject $list -Name 'itemsField') $totalField = [string](Get-VMetricMember -InputObject $list -Name 'totalField') $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output') $idField = Get-VMetricTypeField -TypeName $output -Field 'idField' $nameField = Get-VMetricTypeField -TypeName $output -Field 'nameField' # list.expand: the list answers summaries (a device without its properties), so each item that passes the # name filter is read again through the item endpoint before it is written, one at a time. Resolving names # to ids (-Raw) needs only the summary. $expandTemplate = $null $expandKey = $null $expandMethod = 'GET' $itemDefinition = Get-VMetricMember -InputObject $Entry -Name 'item' if (-not $Raw -and [bool](Get-VMetricMember -InputObject $list -Name 'expand') -and $itemDefinition) { $expandTemplate = [string](Get-VMetricMember -InputObject $itemDefinition -Name 'path') foreach ($placeholder in (Get-VMetricPathPlaceholder -Template $expandTemplate)) { if (-not $PathValues.ContainsKey($placeholder)) { $expandKey = $placeholder } } $itemMethod = [string](Get-VMetricMember -InputObject $itemDefinition -Name 'method') if ($itemMethod) { $expandMethod = $itemMethod } if (-not $expandKey) { $expandTemplate = $null } } # Paging: with a totalField the list is read 100 at a time until total; without one it is read once. $paged = [bool]$totalField $pageSize = 100 $pageNumber = 1 $received = 0 $seen = [System.Collections.Generic.HashSet[string]]::new() $listed = [System.Collections.Generic.List[object]]::new() while ($true) { $pageQuery = [ordered]@{} if ($Query) { foreach ($key in $Query.Keys) { $pageQuery[$key] = $Query[$key] } } if ($paged) { $pageQuery['pageNumber'] = $pageNumber $pageQuery['itemCount'] = $pageSize } $answer = Invoke-VMetricRequest -Method $method -Path $path -Query $pageQuery $items = $null if ($itemsField) { $items = Get-VMetricMember -InputObject $answer -Name $itemsField } if ($null -eq $items -and $answer -is [array]) { $items = $answer } $items = ConvertTo-VMetricArray -InputObject $items $total = $null if ($totalField) { $totalValue = Get-VMetricMember -InputObject $answer -Name $totalField if ($null -ne $totalValue) { $total = [long]$totalValue } } $fresh = 0 foreach ($item in $items) { $id = [string](Get-VMetricMember -InputObject $item -Name $idField) if ($id -and -not $seen.Add($id)) { continue } $fresh++ if ($NameFilter -and [string](Get-VMetricMember -InputObject $item -Name $nameField) -ne $NameFilter) { continue } $listed.Add($item) } $received += $items.Count if (-not $paged -or $items.Count -eq 0 -or $fresh -eq 0) { break } if ($null -ne $total -and $received -ge $total) { break } if ($null -eq $total -and $items.Count -lt $pageSize) { break } $pageNumber++ } $noun = Get-VMetricNounText -CmdletName ([string]$Entry.name) foreach ($item in $listed) { if ($Raw) { $item continue } $id = [string](Get-VMetricMember -InputObject $item -Name $idField) if ($expandTemplate -and $id) { $values = @{} foreach ($key in $PathValues.Keys) { $values[$key] = $PathValues[$key] } $values[$expandKey] = $id try { $full = Invoke-VMetricRequest -Method $expandMethod -Path (Resolve-VMetricPathTemplate -Template $expandTemplate -Values $values) } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } if ($_.Exception -is [VMetricException] -and $_.Exception.StatusCode -eq 404) { $text = "Skipped $noun $(ConvertTo-VMetricSafeText -Text $id): it was removed after the list was read." if ($Cmdlet) { $Cmdlet.WriteWarning($text) } else { Write-Warning $text } continue } throw } $item = Join-VMetricObject -Base $item -Overlay $full } ConvertTo-VMetricOutputObject -InputObject $item -TypeName $output } } function Join-VMetricObject { # The item read through item laid over its list summary: the item's fields win, and what only the # summary has (a name the list resolves, say) stays. [CmdletBinding()] param( [AllowNull()] [object] $Base, [AllowNull()] [object] $Overlay ) if (-not (Test-VMetricObjectLike -InputObject $Overlay)) { return $Base } if (-not (Test-VMetricObjectLike -InputObject $Base)) { return $Overlay } $map = [ordered]@{} foreach ($entry in (Get-VMetricEntryList -InputObject $Base)) { $map[$entry.Name] = $entry.Value } foreach ($entry in (Get-VMetricEntryList -InputObject $Overlay)) { $key = Get-VMetricDictionaryKey -Dictionary $map -Name $entry.Name if ($null -eq $key) { $key = $entry.Name } $map[$key] = $entry.Value } [pscustomobject]$map } function Invoke-VMetricCatalogGet { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Entry, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [System.Management.Automation.PSCmdlet] $Cmdlet ) $arguments = Resolve-VMetricRestArgument -Entry $Entry -Bound $Bound $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output') $item = Get-VMetricMember -InputObject $Entry -Name 'item' $list = Get-VMetricMember -InputObject $Entry -Name 'list' if ($item) { $itemTemplate = [string](Get-VMetricMember -InputObject $item -Name 'path') $itemPath = Resolve-VMetricPathTemplate -Template $itemTemplate -Values $arguments.Path $hasPlaceholders = (Get-VMetricPathPlaceholder -Template $itemTemplate).Count -gt 0 if (($null -ne $itemPath -and $hasPlaceholders) -or -not $list) { if ($null -eq $itemPath) { $needed = (Get-VMetricPathPlaceholder -Template $itemTemplate) -join ', ' throw (New-VMetricException -Message "$($Entry.name) needs a value for: $needed." -Code 'MissingParameter' -Category InvalidArgument) } $method = [string](Get-VMetricMember -InputObject $item -Name 'method') if (-not $method) { $method = 'GET' } $answer = Invoke-VMetricRequest -Method $method -Path $itemPath -Query $arguments.Query ConvertTo-VMetricOutputObject -InputObject $answer -TypeName $output return } } if (-not $list) { throw (New-VMetricException -Message "The catalog entry of $($Entry.name) has neither a list nor an item." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating) } $nameFilter = $null foreach ($param in (Get-VMetricCatalogParameter -Entry $Entry)) { $name = [string](Get-VMetricMember -InputObject $param -Name 'name') if ((Get-VMetricMember -InputObject $param -Name 'filter') -eq 'exactName' -and (Test-VMetricBound -Bound $Bound -Name $name)) { $nameFilter = [string]$Bound[$name] } } Invoke-VMetricCatalogList -Entry $Entry -PathValues $arguments.Path -Query $arguments.Query -NameFilter $nameFilter -Cmdlet $Cmdlet } function Resolve-VMetricItemPath { # Fills an item path: from the bound path params, and the one placeholder left from -Name, looked up # through the entry's resolveIdBy get cmdlet (exact name; ambiguous or unknown names are errors). [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [object] $Entry, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [string] $Template, [Parameter(Mandatory)] [AllowEmptyCollection()] [hashtable] $PathValues ) $values = @{} foreach ($key in $PathValues.Keys) { $values[$key] = $PathValues[$key] } $placeholders = Get-VMetricPathPlaceholder -Template $Template $missing = [System.Collections.Generic.List[string]]::new() foreach ($placeholder in $placeholders) { if (-not $values.ContainsKey($placeholder) -or [string]$values[$placeholder] -eq '') { $missing.Add($placeholder) } } $name = $null if (Test-VMetricBound -Bound $Bound -Name 'Name') { $name = [string]$Bound['Name'] } $noun = Get-VMetricNounText -CmdletName ([string]$Entry.name) if ($missing.Count -eq 1 -and $name) { $resolver = [string](Get-VMetricMember -InputObject $Entry -Name 'resolveIdBy') if (-not $resolver) { throw (New-VMetricException -Message "$($Entry.name) cannot find a $noun by name; use -Id." -Code 'CatalogEntryInvalid' -Category InvalidData) } $getEntry = Get-VMetricCatalogCmdlet -Name $resolver # The parameters the resolver shares with this cmdlet (a child's -Pipeline) go to it as its own # parameters, through its own definitions; its exact-name filter takes -Name. $resolverBound = @{} foreach ($param in (Get-VMetricCatalogParameter -Entry $getEntry)) { $paramName = [string](Get-VMetricMember -InputObject $param -Name 'name') if ($paramName -notin @('Name', 'Id') -and (Test-VMetricBound -Bound $Bound -Name $paramName)) { $resolverBound[$paramName] = $Bound[$paramName] } } $resolverArguments = Resolve-VMetricRestArgument -Entry $getEntry -Bound $resolverBound $query = [ordered]@{} foreach ($key in $resolverArguments.Query.Keys) { $query[$key] = $resolverArguments.Query[$key] } foreach ($param in (Get-VMetricCatalogParameter -Entry $getEntry)) { if ((Get-VMetricMember -InputObject $param -Name 'filter') -eq 'exactName' -and (Get-VMetricMember -InputObject $param -Name 'in') -eq 'query') { $query[[string](Get-VMetricMember -InputObject $param -Name 'key')] = $name } } $found = @(Invoke-VMetricCatalogList -Entry $getEntry -PathValues $resolverArguments.Path -Query $query -NameFilter $name -Raw) $idField = Get-VMetricTypeField -TypeName ([string](Get-VMetricMember -InputObject $getEntry -Name 'output')) -Field 'idField' if ($found.Count -eq 0) { throw (New-VMetricException -Message "No $noun named '$(ConvertTo-VMetricSafeText -Text $name)' was found." -Code 'ResourceNotFound' -Category ObjectNotFound) } if ($found.Count -gt 1) { $ids = ($found | ForEach-Object { ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $_ -Name $idField)) }) -join ', ' throw (New-VMetricException -Message "Several ${noun}s are named '$(ConvertTo-VMetricSafeText -Text $name)' ($ids); use -Id." -Code 'AmbiguousName' -Category InvalidArgument) } $values[$missing[0]] = [string](Get-VMetricMember -InputObject $found[0] -Name $idField) $missing.Clear() } if ($missing.Count -gt 0) { throw (New-VMetricException -Message "$($Entry.name) needs -Id or -Name." -Code 'MissingParameter' -Category InvalidArgument) } $id = $null if ($placeholders.Count -gt 0) { $id = [string]$values[$placeholders[$placeholders.Count - 1]] } # The label a confirmation names the resource by: the id may be one a list answered. $shownName = ConvertTo-VMetricSafeText -Text $name $shownId = ConvertTo-VMetricSafeText -Text $id $label = $noun if ($name -and $id) { $label = "$noun '$shownName' ($shownId)" } elseif ($name) { $label = "$noun '$shownName'" } elseif ($id) { $label = "$noun $shownId" } [pscustomobject]@{ Path = Resolve-VMetricPathTemplate -Template $Template -Values $values Name = $name Id = $id Label = $label } } function Test-VMetricShouldProcess { # $Cmdlet.ShouldProcess, behind a seam: with -Description the three-argument form (the what-if text, the # confirmation question and its caption), otherwise the target and action form. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Asks ShouldProcess of the public cmdlet it is handed, which declares SupportsShouldProcess.')] [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet, [string] $Target, [string] $Action, [string] $Description, [string] $Warning, [string] $Caption ) if ($Description) { return $Cmdlet.ShouldProcess($Description, $Warning, $Caption) } if ($Action) { return $Cmdlet.ShouldProcess($Target, $Action) } return $Cmdlet.ShouldProcess($Target) } function Get-VMetricConfirmImpact { # The ConfirmImpact the public cmdlet declares (its CmdletBinding), else the catalog's, else Medium. [CmdletBinding()] [OutputType([System.Management.Automation.ConfirmImpact])] param( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet, [AllowNull()] [object] $Entry ) $command = $Cmdlet.MyInvocation.MyCommand if ($command -is [System.Management.Automation.FunctionInfo] -and $command.ScriptBlock) { foreach ($attribute in $command.ScriptBlock.Attributes) { if ($attribute -is [System.Management.Automation.CmdletBindingAttribute]) { return $attribute.ConfirmImpact } } } $fromCatalog = [string](Get-VMetricMember -InputObject $Entry -Name 'confirmImpact') if ($fromCatalog) { return [System.Management.Automation.ConfirmImpact]$fromCatalog } return [System.Management.Automation.ConfirmImpact]::Medium } function Test-VMetricWhatIf { # Whether the public cmdlet runs with -WhatIf: given to it, or $WhatIfPreference. (Its -WhatIf is read # from its bound parameters: $PSCmdlet.GetVariableValue does not see the cmdlet's own scope from here.) [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet ) $bound = $Cmdlet.MyInvocation.BoundParameters if ($bound.ContainsKey('WhatIf')) { return [bool]$bound['WhatIf'] } return [bool]$WhatIfPreference } function Test-VMetricPromptActive { # Whether ShouldProcess will print a what-if or ask for confirmation: only then is the preview worth its # round trip. [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet, [Parameter(Mandatory)] [System.Management.Automation.ConfirmImpact] $ConfirmImpact ) if (Test-VMetricWhatIf -Cmdlet $Cmdlet) { return $true } $bound = $Cmdlet.MyInvocation.BoundParameters if ($bound.ContainsKey('Confirm')) { return [bool]$bound['Confirm'] } $preference = [System.Management.Automation.ConfirmImpact]$ConfirmPreference return ($preference -ne [System.Management.Automation.ConfirmImpact]::None -and [int]$ConfirmImpact -ge [int]$preference) } function New-VMetricResourceDeploymentName { # pwsh-<verb>-<type's last segment>-<resource name>, at most 128 characters, no control characters. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds a string; changes no state.')] [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Verb, [Parameter(Mandatory)] [string] $ResourceType, [Parameter(Mandatory)] [string] $Name ) $segment = $ResourceType.Substring($ResourceType.LastIndexOf('/') + 1) $text = [regex]::Replace("pwsh-$($Verb.ToLowerInvariant())-$segment-$Name", '[\p{C}\p{Zl}\p{Zp}]', '') if ($text.Length -gt 128) { $text = $text.Substring(0, 128) } $text } function Format-VMetricPreviewText { # The engine's preview as the what-if shows it: for new, one line per change ("create <type> <name>"); # otherwise every delta as "<path>: <before> -> <after>". Secrets the request carries are masked whatever the # server echoes: a delta at a secret's own path (SecretPath) shows ***, and every value is masked before it # is encoded as JSON (Hide-VMetricSecretValue). [CmdletBinding()] [OutputType([string[]])] param( [AllowNull()] [object] $Preview, [Parameter(Mandatory)] [string] $Kind, [AllowNull()] [string[]] $Secret, # Where the request's resource holds its secrets (Get-VMetricSecretPath). [AllowNull()] [string[]] $SecretPath ) $arrow = [string][char]0x2192 $lines = [System.Collections.Generic.List[string]]::new() foreach ($change in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $Preview -Name 'changes'))) { $changeType = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $change -Name 'changeType')) $type = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $change -Name 'type')) $name = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $change -Name 'name')) $deltas = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $change -Name 'delta') if ($Kind -eq 'new' -or $deltas.Count -eq 0) { $lines.Add("$changeType $type $name") } if ($Kind -ne 'new') { foreach ($delta in $deltas) { $rawPath = [string](Get-VMetricMember -InputObject $delta -Name 'path') $path = ConvertTo-VMetricSafeText -Text $rawPath $before = Format-VMetricValue -InputObject (Hide-VMetricSecretValue -InputObject (Get-VMetricMember -InputObject $delta -Name 'before') -Secret $Secret -Path $rawPath -SecretPath $SecretPath) $after = Format-VMetricValue -InputObject (Hide-VMetricSecretValue -InputObject (Get-VMetricMember -InputObject $delta -Name 'after') -Secret $Secret -Path $rawPath -SecretPath $SecretPath) $lines.Add("${path}: $before $arrow $after") } } $failure = [string](Get-VMetricMember -InputObject $change -Name 'error') if ($failure) { $lines.Add(" error: $(ConvertTo-VMetricSafeText -Text $failure)") } } $masked = foreach ($line in $lines) { Hide-VMetricSecretText -Text $line -Secret $Secret } return , [string[]]@($masked) } function Write-VMetricPreviewDiagnostic { # A preview's diagnostics through the public cmdlet: errors as errors, warnings as warnings. Answers # whether there was an error (the deployment would be refused). [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet, [AllowNull()] [object] $Preview, [AllowNull()] [string[]] $Secret, [AllowNull()] [object] $TargetObject ) $hasError = $false foreach ($diagnostic in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $Preview -Name 'diagnostics'))) { $severity = [string](Get-VMetricMember -InputObject $diagnostic -Name 'severity') $code = [string](Get-VMetricMember -InputObject $diagnostic -Name 'code') $message = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $diagnostic -Name 'message')) $text = Hide-VMetricSecretText -Text "$(ConvertTo-VMetricSafeText -Text $code): $message" -Secret $Secret if ($severity -eq 'error') { $hasError = $true $exception = New-VMetricException -Message $text -Code $(if ($code) { $code } else { 'PreviewError' }) -Category InvalidData $Cmdlet.WriteError((New-VMetricErrorRecord -Exception $exception -TargetObject $TargetObject)) } elseif ($severity -eq 'warning') { $Cmdlet.WriteWarning($text) } } return $hasError } function Wait-VMetricDeploymentCompletion { # Polls GET /deployments/{id} every IntervalSec seconds while the deployment runs, with Write-Progress per # operation and a verbose line as each operation finishes. Answers the final deployment. [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Deployment, [System.Collections.IDictionary] $Session, [double] $IntervalSec = 2, [int] $TimeoutMinutes = 45 ) $id = [string](Get-VMetricMember -InputObject $Deployment -Name 'id') $shownId = ConvertTo-VMetricSafeText -Text $id $name = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $Deployment -Name 'name')) $activity = "Deployment '$name'" $reported = @{} $deadline = [datetime]::UtcNow.AddMinutes($TimeoutMinutes) $request = @{ Method = 'GET'; Path = "/deployments/$(ConvertTo-VMetricPathSegment -Value $id -Name 'The deployment id')" } if ($Session) { $request.Session = $Session } try { while ($true) { $operations = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $Deployment -Name 'operations') $finished = 0 $running = $null foreach ($operation in $operations) { $status = [string](Get-VMetricMember -InputObject $operation -Name 'status') $seq = [string](Get-VMetricMember -InputObject $operation -Name 'seq') if ($status -in @('succeeded', 'failed', 'skipped')) { $finished++ if (-not $reported.ContainsKey($seq)) { $reported[$seq] = $true Write-Verbose ('[{0}/{1}] {2} {3} ''{4}'': {5}' -f (ConvertTo-VMetricSafeText -Text $seq), $operations.Count, (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $operation -Name 'operation'))), (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $operation -Name 'type'))), (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $operation -Name 'name'))), $status) } } elseif ($status -eq 'running' -and -not $running) { $running = $operation } } if ([string](Get-VMetricMember -InputObject $Deployment -Name 'status') -ne 'running') { return $Deployment } if ([datetime]::UtcNow -ge $deadline) { throw (New-VMetricException -Message "Deployment '$name' ($shownId) is still running after $TimeoutMinutes minutes. Follow it with Get-VMetricDeployment -Id $shownId." ` -Code 'DeploymentStillRunning' -Category OperationTimeout) } $percent = 0 if ($operations.Count -gt 0) { $percent = [int][Math]::Floor(100 * $finished / $operations.Count) } $progress = @{ Activity = $activity Status = "$finished of $($operations.Count) operations complete" PercentComplete = $percent } if ($running) { $progress.CurrentOperation = '{0} {1} ''{2}''' -f (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $running -Name 'operation'))), (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $running -Name 'type'))), (ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $running -Name 'name'))) } Write-Progress @progress Wait-VMetricInterval -Seconds $IntervalSec $Deployment = Invoke-VMetricRequest @request } } finally { Write-Progress -Activity $activity -Completed } } function Add-VMetricOneTimeOutput { # A write's one-time outputs (a new director's apiKey and installScript) on the emitted object, as ApiKey and # InstallScript, with a warning: they are shown once. [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet, [Parameter(Mandatory)] [object] $InputObject, [AllowNull()] [object] $Outputs, [Parameter(Mandatory)] [string] $Label ) $names = [System.Collections.Generic.List[string]]::new() foreach ($entry in (Get-VMetricEntryList -InputObject $Outputs)) { if (-not $entry.Name) { continue } $name = $entry.Name.Substring(0, 1).ToUpperInvariant() + $entry.Name.Substring(1) $InputObject | Add-Member -NotePropertyName $name -NotePropertyValue $entry.Value -Force $names.Add((ConvertTo-VMetricSafeText -Text $name)) } if ($names.Count -gt 0) { $Cmdlet.WriteWarning("$Label returned $($names -join ' and '): shown once; copy it now. It cannot be shown again.") } } function Get-VMetricWrittenResource { # The resource a write created or changed, read back through the get cmdlet's item by the id in the # operation record. Falls back to the template form when there is no such item. [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Entry, [AllowNull()] [AllowEmptyString()] [string] $ResourceId, [Parameter(Mandatory)] [System.Collections.IDictionary] $Resource ) $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output') $getEntry = Find-VMetricReadCmdlet -Entry $Entry $item = Get-VMetricMember -InputObject $getEntry -Name 'item' if ($item -and $ResourceId) { $template = [string](Get-VMetricMember -InputObject $item -Name 'path') $placeholders = Get-VMetricPathPlaceholder -Template $template $values = @{} if ($placeholders.Count -eq 1) { $values[$placeholders[0]] = $ResourceId } elseif ($placeholders.Count -eq 2 -and $Resource.Contains('parent')) { $parent = $Resource['parent'] $values[$placeholders[0]] = (Resolve-VMetricResourceId -Type $parent['type'] -Name @([string]$parent['name']))[0] $values[$placeholders[1]] = $ResourceId } $path = Resolve-VMetricPathTemplate -Template $template -Values $values if ($path) { if (-not $output) { $output = [string](Get-VMetricMember -InputObject $getEntry -Name 'output') } $answer = Invoke-VMetricRequest -Method GET -Path $path return (ConvertTo-VMetricOutputObject -InputObject $answer -TypeName $output) } } $lookup = [ordered]@{ type = $Resource['type']; name = $Resource['name'] } if ($Resource.Contains('parent')) { $lookup['parent'] = $Resource['parent'] } $read = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($lookup) }) $current = (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $read -Name 'resources')) | Select-Object -First 1 $object = [ordered]@{ id = [string](Get-VMetricMember -InputObject $current -Name 'id') name = $Resource['name'] properties = Get-VMetricMember -InputObject $current -Name 'properties' } ConvertTo-VMetricOutputObject -InputObject $object -TypeName $output } function Format-VMetricResourceName { # A nameFormat such as "{Device}/{Target}" filled with the parameters' values as given. [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string] $Format, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [string] $CmdletName ) $builder = [System.Text.StringBuilder]::new() $position = 0 foreach ($match in [regex]::Matches($Format, '\{([^}]+)\}')) { [void]$builder.Append($Format.Substring($position, $match.Index - $position)) $parameter = $match.Groups[1].Value if (-not (Test-VMetricBound -Bound $Bound -Name $parameter) -or [string]$Bound[$parameter] -eq '') { throw (New-VMetricException -Message "$CmdletName needs -$parameter." -Code 'MissingParameter' -Category InvalidArgument) } [void]$builder.Append([string]$Bound[$parameter]) $position = $match.Index + $match.Length } [void]$builder.Append($Format.Substring($position)) $builder.ToString() } function Invoke-VMetricCatalogWrite { # new, set, enable and disable: one resource through the deployment engine. [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Entry, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet ) $kind = [string]$Entry.kind $resourceType = [string](Get-VMetricMember -InputObject $Entry -Name 'resourceType') if (-not $resourceType) { throw (New-VMetricException -Message "The catalog entry of $($Entry.name) names no resourceType." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating) } $noun = Get-VMetricNounText -CmdletName ([string]$Entry.name) # The resource in the deployment property model, at the version the catalog was written for. $resource = [ordered]@{ type = $resourceType } $apiVersion = [string](Get-VMetricCatalog).ApiVersion if ($apiVersion) { $resource['apiVersion'] = $apiVersion } $resource['name'] = $null $resource['properties'] = [ordered]@{} $parentName = $null $parentType = $null $assignments = [System.Collections.Generic.List[object]]::new() foreach ($param in (Get-VMetricCatalogParameter -Entry $Entry)) { $name = [string](Get-VMetricMember -InputObject $param -Name 'name') $target = [string](Get-VMetricMember -InputObject $param -Name 'target') if (-not $target -or -not (Test-VMetricBound -Bound $Bound -Name $name)) { continue } $value = Resolve-VMetricParamValue -Param $param -Value $Bound[$name] if ($target -eq 'name') { $resource['name'] = [string]$value } elseif ($target -eq 'parent.name' -or $target -eq 'parent') { $parentName = [string]$value } elseif ($target -eq 'parent.type') { $parentType = [string]$value } else { $assignments.Add([pscustomobject]@{ Name = $name Target = $target Transform = [string](Get-VMetricMember -InputObject $param -Name 'transform') Value = $value }) } } # nameFormat builds the name from parameters when none targets it: a quick route is '<device>/<target>', # from the names given, before they are resolved to ids. $nameFormat = [string](Get-VMetricMember -InputObject $Entry -Name 'nameFormat') if (-not $resource['name'] -and $nameFormat) { $resource['name'] = Format-VMetricResourceName -Format $nameFormat -Bound $Bound -CmdletName ([string]$Entry.name) } $resourceName = [string]$resource['name'] if (-not $resourceName) { throw (New-VMetricException -Message "$($Entry.name) needs -Name." -Code 'MissingParameter' -Category InvalidArgument) } if ($parentName) { if (-not $parentType) { $parentType = [string](Get-VMetricMember -InputObject $Entry -Name 'parentType') } if (-not $parentType -and ($resourceType.Split('/').Count -gt 2)) { $parentType = $resourceType.Substring(0, $resourceType.LastIndexOf('/')) } $resource['parent'] = [ordered]@{ type = $parentType; name = $parentName } } # The name may be a server's, piped from a get: the label a prompt and every message show is cleaned. $shownName = ConvertTo-VMetricSafeText -Text $resourceName $label = "$noun '$shownName'" # The current state: set, enable and disable change it; new must not find it. $lookup = [ordered]@{ type = $resourceType; name = $resourceName } if ($resource.Contains('parent')) { $lookup['parent'] = $resource['parent'] } $read = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/read' -Body ([ordered]@{ resources = @($lookup) }) -AsHashtable $current = (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $read -Name 'resources')) | Select-Object -First 1 $exists = [bool](Get-VMetricMember -InputObject $current -Name 'exists') if ($kind -eq 'new') { if ($exists) { $setName = 'Set-' + ([string]$Entry.name).Substring(([string]$Entry.name).IndexOf('-') + 1) throw (New-VMetricException -Message "A $noun named '$shownName' already exists. Use $setName to change it." -Code 'ResourceExists' -Category ResourceExists) } } else { if (-not $exists) { throw (New-VMetricException -Message "No $noun named '$shownName' was found." -Code 'ResourceNotFound' -Category ObjectNotFound) } $properties = Copy-VMetricData -InputObject (Get-VMetricMember -InputObject $current -Name 'properties') if ($properties -isnot [System.Collections.IDictionary]) { $properties = [ordered]@{} } $resource['properties'] = $properties } foreach ($assignment in $assignments) { if ($assignment.Transform -eq 'merge') { Merge-VMetricPathValue -Target $resource -Path $assignment.Target -Value $assignment.Value -ParameterName $assignment.Name } elseif ($assignment.Transform -like 'relationOf:*') { Set-VMetricRelationValue -Target $resource -Path $assignment.Target -Kind $assignment.Transform.Substring('relationOf:'.Length) -Value $assignment.Value } else { Set-VMetricPathValue -Target $resource -Path $assignment.Target -Value $assignment.Value } } if ($kind -eq 'enable') { Set-VMetricPathValue -Target $resource -Path 'properties.status' -Value 'active' } elseif ($kind -eq 'disable') { Set-VMetricPathValue -Target $resource -Path 'properties.status' -Value 'passive' } elseif ($kind -eq 'set' -and $assignments.Count -eq 0) { $Cmdlet.WriteWarning("Nothing to change on $label`: give at least one property.") return } $verbText = @{ new = 'Create'; set = 'Update'; enable = 'Enable'; disable = 'Disable' }[$kind] $description = "$verbText $label" $secrets = Get-VMetricSecretText -InputObject $resource $impact = Get-VMetricConfirmImpact -Cmdlet $Cmdlet -Entry $Entry if (Test-VMetricPromptActive -Cmdlet $Cmdlet -ConfirmImpact $impact) { $preview = Invoke-VMetricRequest -Method POST -Path '/deployments/resources/preview' -Body ([ordered]@{ resources = @($resource) }) $refused = Write-VMetricPreviewDiagnostic -Cmdlet $Cmdlet -Preview $preview -Secret $secrets -TargetObject $label $lines = Format-VMetricPreviewText -Preview $preview -Kind $kind -Secret $secrets -SecretPath (Get-VMetricSecretPath -InputObject $resource) $changeTypes = [System.Collections.Generic.List[string]]::new() foreach ($change in (ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $preview -Name 'changes'))) { $changeTypes.Add([string](Get-VMetricMember -InputObject $change -Name 'changeType')) } if ($kind -ne 'new' -and $changeTypes.Count -gt 0 -and -not ($changeTypes | Where-Object { $_ -ne 'noChange' })) { $description = "${description}: no change" } elseif ($lines.Count -gt 0) { $description = "${description}:" + [Environment]::NewLine + (($lines | ForEach-Object { " $_" }) -join [Environment]::NewLine) } if ($refused -and -not (Test-VMetricWhatIf -Cmdlet $Cmdlet)) { return } } $question = "$description$([Environment]::NewLine)Continue?" if (-not (Test-VMetricShouldProcess -Cmdlet $Cmdlet -Description $description -Warning $question -Caption "$verbText $noun")) { return } $deploymentName = New-VMetricResourceDeploymentName -Verb $kind -ResourceType $resourceType -Name $resourceName $deployment = Invoke-VMetricRequest -Method POST -Path '/deployments/resources' -Query ([ordered]@{ wait = 'true' }) ` -Body ([ordered]@{ name = $deploymentName; resources = @($resource) }) $deployment = Wait-VMetricDeploymentCompletion -Deployment $deployment $operations = ConvertTo-VMetricArray -InputObject (Get-VMetricMember -InputObject $deployment -Name 'operations') $operation = $null foreach ($candidate in $operations) { if ([string](Get-VMetricMember -InputObject $candidate -Name 'type') -eq $resourceType -and [string](Get-VMetricMember -InputObject $candidate -Name 'name') -eq $resourceName) { $operation = $candidate } } if (-not $operation -and $operations.Count -gt 0) { $operation = $operations[$operations.Count - 1] } $status = [string](Get-VMetricMember -InputObject $deployment -Name 'status') if ($status -ne 'succeeded') { $failure = Get-VMetricMember -InputObject $operation -Name 'error' if (-not $failure) { $failure = Get-VMetricMember -InputObject $deployment -Name 'error' } $code = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $failure -Name 'code')) if (-not $code) { $code = 'DEPLOYMENT_FAILED' } $message = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $failure -Name 'message')) if (-not $message) { $message = "the deployment ended $(ConvertTo-VMetricSafeText -Text $status)" } $masked = Hide-VMetricSecretText -Text $message -Secret $secrets $details = $deployment if ($masked -ne $message) { $message = $masked $details = $null } $deploymentId = ConvertTo-VMetricSafeText -Text ([string](Get-VMetricMember -InputObject $deployment -Name 'id')) throw (New-VMetricException -Message "$verbText $label failed: $message ($code; deployment $deploymentName, id $deploymentId)" ` -Code $code -Category InvalidOperation -Details $details) } $resourceId = [string](Get-VMetricMember -InputObject $operation -Name 'resourceId') try { $object = Get-VMetricWrittenResource -Entry $Entry -ResourceId $resourceId -Resource $resource } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $Cmdlet.WriteWarning("$verbText $label succeeded, but reading it back failed: $($_.Exception.Message)") $object = ConvertTo-VMetricOutputObject -InputObject ([ordered]@{ id = $resourceId; name = $resourceName }) -TypeName ([string]$Entry.output) } if ($object) { Add-VMetricOneTimeOutput -Cmdlet $Cmdlet -InputObject $object -Outputs (Get-VMetricMember -InputObject $operation -Name 'outputs') -Label $label $object } } function Invoke-VMetricCatalogRemove { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Entry, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet ) $item = Get-VMetricMember -InputObject $Entry -Name 'item' $template = [string](Get-VMetricMember -InputObject $item -Name 'path') if (-not $template) { throw (New-VMetricException -Message "The catalog entry of $($Entry.name) has no item path." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating) } $method = [string](Get-VMetricMember -InputObject $item -Name 'method') if (-not $method) { $method = 'DELETE' } $arguments = Resolve-VMetricRestArgument -Entry $Entry -Bound $Bound $target = Resolve-VMetricItemPath -Entry $Entry -Bound $Bound -Template $template -PathValues $arguments.Path if (-not (Test-VMetricShouldProcess -Cmdlet $Cmdlet -Target $target.Label -Action 'Remove')) { return } $null = Invoke-VMetricRequest -Method $method -Path $target.Path -Query $arguments.Query } function Invoke-VMetricCatalogAction { [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Entry, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet ) $request = Get-VMetricMember -InputObject $Entry -Name 'action' foreach ($field in @('request', 'item')) { if (-not $request) { $request = Get-VMetricMember -InputObject $Entry -Name $field } } $template = [string](Get-VMetricMember -InputObject $request -Name 'path') if (-not $template) { throw (New-VMetricException -Message "The catalog entry of $($Entry.name) has no request path." -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating) } $method = [string](Get-VMetricMember -InputObject $request -Name 'method') if (-not $method) { $method = 'POST' } $arguments = Resolve-VMetricRestArgument -Entry $Entry -Bound $Bound $target = Resolve-VMetricItemPath -Entry $Entry -Bound $Bound -Template $template -PathValues $arguments.Path if ($method -ne 'GET' -and -not (Test-VMetricShouldProcess -Cmdlet $Cmdlet -Target $target.Label)) { return } $call = @{ Method = $method; Path = $target.Path; Query = $arguments.Query } if ($method -in @('POST', 'PUT', 'PATCH')) { $call.Body = $arguments.Body } $answer = Invoke-VMetricRequest @call if ($null -eq $answer) { return } $output = [string](Get-VMetricMember -InputObject $Entry -Name 'output') $itemsField = [string](Get-VMetricMember -InputObject $request -Name 'itemsField') $items = $null if ($itemsField) { $items = Get-VMetricMember -InputObject $answer -Name $itemsField } elseif ($answer -is [array]) { $items = $answer } if ($null -ne $items) { foreach ($entry in (ConvertTo-VMetricArray -InputObject $items)) { ConvertTo-VMetricOutputObject -InputObject $entry -TypeName $output } return } ConvertTo-VMetricOutputObject -InputObject $answer -TypeName $output } function Select-VMetricPipelineBound { # The parameters the pipeline object being processed bound, with those given on the command line. # PowerShell keeps a value a previous object bound by property name in $PSBoundParameters when the next # object lacks the property (it resets only the variable): without this, Remove-VMetricDevice would target # the previous object's id under a prompt naming the current one, and New-VMetricDevice would copy the # previous object's port. A parameter that did not come from the command line stays only when it is bound # by value, or when the object has a property of its name or of one of its aliases. [CmdletBinding()] [OutputType([System.Collections.IDictionary])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, # The keys of $PSBoundParameters in the cmdlet's begin block. [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $CommandLine, # The pipeline object being processed ($_ in the process block). [AllowNull()] [object] $InputObject, [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet ) $parameters = $Cmdlet.MyInvocation.MyCommand.Parameters $fresh = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($key in $Bound.Keys) { $name = [string]$key if ($CommandLine -contains $name) { $fresh[$name] = $Bound[$key] continue } $metadata = $null if ($parameters -and -not $parameters.TryGetValue($name, [ref] $metadata)) { $metadata = $null } if (-not $metadata) { continue } $byValue = $false foreach ($attribute in $metadata.Attributes) { if ($attribute -is [System.Management.Automation.ParameterAttribute] -and $attribute.ValueFromPipeline) { $byValue = $true } } $carried = $false if ($null -ne $InputObject) { foreach ($candidate in (@($metadata.Name) + @($metadata.Aliases))) { if ($InputObject -is [System.Collections.IDictionary]) { if ($InputObject.Contains($candidate)) { $carried = $true } } elseif ($InputObject.psobject.Properties[$candidate]) { $carried = $true } } } if ($byValue -or $carried) { $fresh[$name] = $Bound[$key] } else { Write-Verbose "-$name is not on this pipeline object; the previous object's value is not used." } } return , $fresh } function Invoke-VMetricCatalogCmdlet { # The body of every generated cmdlet: Invoke-VMetricCatalogCmdlet -Name '<Cmdlet>' -Bound $PSBoundParameters # -Cmdlet $PSCmdlet -CommandLine $commandLine -InputObject $_. Output, errors, warnings and ShouldProcess all # go through the public cmdlet, so they name it and honour its -WhatIf, -Confirm, -ErrorAction and # -WarningAction. [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [AllowEmptyCollection()] [System.Collections.IDictionary] $Bound, [Parameter(Mandatory)] [System.Management.Automation.PSCmdlet] $Cmdlet, # The parameters given on the command line (the stub's begin block); with the pipeline object below, # what tells this object's own values from those a previous object left behind. [AllowNull()] [AllowEmptyCollection()] [string[]] $CommandLine, [AllowNull()] [object] $InputObject ) if ($null -ne $CommandLine) { $Bound = Select-VMetricPipelineBound -Bound $Bound -CommandLine $CommandLine -InputObject $InputObject -Cmdlet $Cmdlet } try { $entry = Get-VMetricCatalogCmdlet -Name $Name $kind = [string](Get-VMetricMember -InputObject $entry -Name 'kind') if ($kind -eq 'get') { Invoke-VMetricCatalogGet -Entry $entry -Bound $Bound -Cmdlet $Cmdlet } elseif ($kind -in @('new', 'set', 'enable', 'disable')) { Invoke-VMetricCatalogWrite -Entry $entry -Bound $Bound -Cmdlet $Cmdlet } elseif ($kind -eq 'remove') { Invoke-VMetricCatalogRemove -Entry $entry -Bound $Bound -Cmdlet $Cmdlet } elseif ($kind -eq 'action') { Invoke-VMetricCatalogAction -Entry $entry -Bound $Bound -Cmdlet $Cmdlet } else { throw (New-VMetricException -Message "$Name is of kind '$kind', which is not served by Invoke-VMetricCatalogCmdlet." ` -Code 'CatalogEntryInvalid' -Category InvalidData -Terminating) } } catch { if (Test-VMetricFlowControl -ErrorRecord $_) { throw } $record = Hide-VMetricSecretInError -ErrorRecord $_ -Secret (Get-VMetricSecretText -InputObject $Bound) Write-VMetricCmdletError -Cmdlet $Cmdlet -ErrorRecord $record } } |