Private/Catalog.ps1
|
# Copyright (c) 2026 Broadcom. All Rights Reserved. # Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc. # and/or its subsidiaries. # # ============================================================================= # # SOFTWARE LICENSE AGREEMENT # # Copyright (c) CA, Inc. All rights reserved. # # You are hereby granted a non-exclusive, worldwide, royalty-free license # under CA, Inc.'s copyrights to use, copy, modify, and distribute this # software in source code or binary form for use in connection with CA, Inc. # products. # # This copyright notice shall be included in all copies or substantial # portions of the software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # #region Catalog function Test-VcfCheckStrictJson { <# .SYNOPSIS Throws unless a file contains strictly valid JSON. .DESCRIPTION PowerShell's own ConvertFrom-Json is lenient about a trailing comma before a closing `}`/`]` - it silently accepts what is not actually valid JSON. Data/CheckCatalog.json is also read by Tools/Start-VcfCheckServer.py via Python's strict json.load, so a trailing comma that ConvertFrom-Json waves through still breaks the running server's /api/checks endpoint with a 500 - confirmed live (2026-08-04): the PowerShell engine ran every check normally while the file had a trailing comma, because nothing on the PowerShell side ever parsed it strictly. Uses System.Text.Json.JsonDocument's default (strict) parsing options - part of the PowerShell 7 runtime already, no Add-Type needed - to catch that class of bug before ConvertFrom-Json ever runs. .PARAMETER Path Path to the JSON file to validate. .OUTPUTS None. Throws [System.InvalidOperationException] when the file is not strictly valid JSON. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Path ) $content = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop $jsonDocument = $null try { $jsonDocument = [System.Text.Json.JsonDocument]::Parse($content) } catch { throw [System.InvalidOperationException]::new("`"$Path`" is not strictly valid JSON (e.g. a trailing comma) - this can silently pass PowerShell's own ConvertFrom-Json while still breaking Python's json.load reading the same file: $($_.Exception.Message)") } finally { if ($jsonDocument) { $jsonDocument.Dispose() } } } function Get-VcfCheckCatalog { <# .SYNOPSIS Loads and caches Data/CheckCatalog.json. .DESCRIPTION The catalog is the single source of truth mapping a check ID to its implementing function, product area, blocking flag, and required connections. Discovery is deliberately explicit (this catalog) rather than reflection over Get-Command -Name 'Test-Vcf*', so check ordering is deterministic and metadata that reflection can't carry (Blocking, RequiresConnections) has a home. .PARAMETER Path Path to CheckCatalog.json. Defaults to $env:VcfCheckBaseDirectory\Data\CheckCatalog.json, falling back to the module's bundled copy if the environment variable is unset (useful for tests that haven't called Initialize-VcfCheck). .PARAMETER Force Bypasses the in-memory cache and reloads from disk. .OUTPUTS [Hashtable] keyed by check id. .EXAMPLE $catalog = Get-VcfCheckCatalog $catalog['sddc_lock_table'].Function #> [CmdletBinding()] [OutputType([Hashtable])] Param ( [Parameter(Mandatory = $false)] [AllowEmptyString()] [String]$Path = '', [Parameter(Mandatory = $false)] [Switch]$Force ) if ($Script:VcfCheckCatalogCache -and -not $Force) { return $Script:VcfCheckCatalogCache } $resolvedPath = $Path if ([String]::IsNullOrWhiteSpace($resolvedPath)) { if (-not [String]::IsNullOrWhiteSpace($env:VcfCheckBaseDirectory)) { $candidate = Join-Path -Path $env:VcfCheckBaseDirectory.Trim() -ChildPath 'Data/CheckCatalog.json' if (Test-Path -LiteralPath $candidate) { $resolvedPath = $candidate } } if ([String]::IsNullOrWhiteSpace($resolvedPath)) { $moduleRoot = Split-Path -Parent $PSScriptRoot $resolvedPath = Join-Path -Path $moduleRoot -ChildPath 'Data/CheckCatalog.json' } } if (-not (Test-Path -LiteralPath $resolvedPath -PathType Leaf)) { throw [System.InvalidOperationException]::new("Check catalog not found at `"$resolvedPath`".") } Test-VcfCheckStrictJson -Path $resolvedPath try { $raw = Get-Content -LiteralPath $resolvedPath -Raw -ErrorAction Stop | ConvertFrom-Json -Depth 10 -AsHashtable -ErrorAction Stop } catch { # ConvertFrom-Json's own error (e.g. "unexpected character... line 1, position 25") never # names the file it was parsing - unhelpful when this same catalog is also read by # Tools/Start-VcfCheckServer.py, so a syntax typo could otherwise be diagnosed from # either side without knowing which file to actually go fix. throw [System.InvalidOperationException]::new("`"$resolvedPath`" is not valid JSON: $($_.Exception.Message)") } $Script:VcfCheckCatalogCache = $raw return $raw } function Resolve-VcfCheckCheckList { <# .SYNOPSIS Resolves an ordered list of checks to run from the catalog. .DESCRIPTION Cross-references Get-VcfCheckCatalog to build a de-duplicated, ordered dispatch list. When CheckId is empty, every check in the catalog is returned (in catalog order) - the "full run" path. .PARAMETER CheckId One or more explicit check IDs to run. Empty (default) runs every check in the catalog. .OUTPUTS [PSCustomObject[]] each with Id, Function, Area, DisplayName, Blocking, RequiresConnections, RequiresSddcManagerRootCredential. .EXAMPLE Resolve-VcfCheckCheckList -CheckId 'sddc_lock_table', 'sddc_bom_check' #> [CmdletBinding()] [OutputType([PSObject[]])] Param ( [Parameter(Mandatory = $false)] [String[]]$CheckId = @() ) $catalog = Get-VcfCheckCatalog $orderedIds = @() if ($CheckId.Count -gt 0) { $seenIds = [System.Collections.Generic.HashSet[String]]::new() foreach ($id in $CheckId) { if ($seenIds.Add($id)) { $orderedIds += $id } } } else { $orderedIds = @($catalog.Keys) } $resolved = [System.Collections.Generic.List[PSObject]]::new() foreach ($id in $orderedIds) { if (-not $catalog.ContainsKey($id)) { throw [System.InvalidOperationException]::new("Check id `"$id`" is not present in the check catalog.") } $entry = $catalog[$id] $resolved.Add([PSCustomObject]@{ Id = $id Function = $entry.function Area = $entry.area DisplayName = $entry.displayName Blocking = [bool]$entry.blocking RequiresConnections = @($entry.requiresConnections) RequiresSddcManagerRootCredential = [bool]$entry.requiresSddcManagerRootCredential }) } return $resolved.ToArray() } #endregion Catalog |