Private/Test-AzLocalAllowedUpdateVersionsString.ps1
|
function Test-AzLocalAllowedUpdateVersionsString { <# .SYNOPSIS Validates and parses a semicolon-separated 'allowedUpdateVersions' string. Returns the deduplicated [string[]] on success, or $null when validation fails (errors appended to the supplied list). .DESCRIPTION Shared between the top-level and per-row validation paths in Get-AzLocalApplyUpdatesScheduleConfig. Rules: - Value must be a non-empty string. Empty / whitespace-only = error (operators almost always mean "don't filter" by omitting the field entirely). - Tokens are ';'-separated. Each token is trimmed. - Empty tokens after trim are rejected ('a;;b' is a typo). - Whitespace inside a token is rejected (Azure Local solution update names + versions do not contain spaces). - 'Latest' (case-insensitive) is a RESERVED SENTINEL meaning "no constraint - install the latest Ready update on each cluster" (the historic v0.7.88 default behaviour). It is canonicalised to 'Latest' (PascalCase) in the returned array. - 'Latest' MUST appear alone. Mixing 'Latest' with explicit update names / versions in the same field is rejected with a clear error - the operator's intent is ambiguous. (Cross-row UNION with 'Latest' is handled by the resolver, not here; this helper only validates a single field.) - Single quotes wrapping the whole string come from YAML and are stripped by the parser already - this helper does not try to re-strip them. Returns a deduplicated array preserving first-occurrence order so logs / audit reports show the operator's authored order. .PARAMETER Raw The raw value from the parser. May be $null / non-string when callers pass through an unusual YAML node; treated as error. .PARAMETER Location Human-readable location string used in error messages (e.g. "top-level 'allowedUpdateVersions'" or "schedule[3] (line 42) 'allowedUpdateVersions'"). .PARAMETER Errors System.Collections.Generic.List[string] - errors collected by the caller. New errors are appended in place; no return value is used to signal failure beyond the $null return. .OUTPUTS [string[]] on success; $null on failure. #> [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory = $true)] [AllowNull()] $Raw, [Parameter(Mandatory = $true)] [string]$Location, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[string]]$Errors ) if ($null -eq $Raw -or $Raw -isnot [string]) { $Errors.Add("$Location must be a non-empty semicolon-separated string. Use 'Latest' (no constraint - install latest Ready update) or a list of Azure Local solution-update names / version strings (e.g. '10.2604.0.123;10.2610.0.456'). Got: $(if ($null -eq $Raw) { '<null>' } else { $Raw.GetType().FullName })") | Out-Null return $null } $raw = [string]$Raw if ([string]::IsNullOrWhiteSpace($raw)) { $Errors.Add("$Location is empty or whitespace-only. Set to 'Latest' to keep the default 'install the latest Ready update' behaviour, or to a non-empty semicolon-separated list of update versions to install (e.g. '10.2604.0.123;10.2610.0.456').") | Out-Null return $null } $tokens = $raw -split ';' $seen = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase) $result = New-Object System.Collections.Generic.List[string] $bad = $false $hasLatest = $false $hasExplicit = $false foreach ($tok in $tokens) { $t = $tok.Trim() if ([string]::IsNullOrEmpty($t)) { $Errors.Add("$Location contains an empty token (likely a stray ';' - e.g. '...;;...' or trailing ';'). Got: '$raw'.") | Out-Null $bad = $true continue } if ($t -match '\s') { $Errors.Add("$Location token '$t' contains whitespace. Azure Local solution-update names and version strings do not contain spaces. Got: '$raw'.") | Out-Null $bad = $true continue } # Canonicalise the reserved 'Latest' sentinel to PascalCase so # downstream consumers can do a simple ordinal compare. if ([string]::Equals($t, 'Latest', [System.StringComparison]::OrdinalIgnoreCase)) { $t = 'Latest' $hasLatest = $true } else { $hasExplicit = $true } if ($seen.Add($t)) { $result.Add($t) | Out-Null } } if ($bad) { return $null } if ($result.Count -eq 0) { # Defensive: split + trim consumed everything. Treat as empty. $Errors.Add("$Location resolved to zero tokens after split-and-trim. Got: '$raw'.") | Out-Null return $null } if ($hasLatest -and $hasExplicit) { $Errors.Add("$Location mixes the reserved sentinel 'Latest' with explicit update names / version strings. 'Latest' must appear alone (it already means 'no constraint - install the latest Ready update'). Either set to 'Latest', or list only explicit versions (e.g. '10.2604.0.123;10.2610.0.456'). Got: '$raw'.") | Out-Null return $null } return ,$result.ToArray() } |