Private/Resolve-AzLocalForceAllowList.ps1

function Resolve-AzLocalForceAllowList {
    ########################################
    <#
    .SYNOPSIS
        Resolves the effective allowedUpdateVersions allow-list to honor for a
        FORCE (break-glass) run, given the operator's manual UpdateRing input
        (which may be a single ring, a ';'-joined list, or the '***' wildcard)
        and a parsed apply-updates schedule (schema v2).
 
    .DESCRIPTION
        v0.9.15 helper for the ForceImmediateUpdate path of
        Resolve-AzLocalPipelineUpdateRing.
 
        A FORCE run deliberately bypasses the schedule WINDOW (day/week/time
        gating) so an operator can patch outside the maintenance window. It
        must NOT, however, bypass the version allow-list: when a specific ring
        (or the fleet-wide global default) constrains which update versions are
        permitted, that constraint still applies to the forced run. Only when
        NO allow-list is configured for the target ring(s) or globally does the
        run fall back to "install the latest Ready update".
 
        Precedence (matching the module's scheduled-path resolver, but matching
        rows by RING instead of by date/window):
 
          1. PER-RING OVERRIDE (highest): the UNION of allowedUpdateVersions
             overrides across every schedule row that covers one of the target
             rings (a row 'rings' value of '***' is a wildcard covering every
             ring; a target ring of '***'/'*' means "all rings" and matches
             every row). Rows WITHOUT an explicit override contribute nothing.
             When any covering row supplies an override the top-level global is
             IGNORED (Source = 'RowOverride').
          2. TOP-LEVEL GLOBAL DEFAULT: when no covering row supplies an
             override, the schedule's top-level allowedUpdateVersions is used
             (Source = 'TopLevel').
          3. NONE: neither present -> empty allow-list (Source = 'None').
 
        The reserved sentinel 'Latest' (case-insensitive, appearing ALONE in
        the effective list) means "no constraint - install the latest Ready
        update". In that case EffectiveAllowList is returned empty and IsLatest
        is $true, so the caller emits no allow-list filter.
 
        Pure decision function: NO Azure calls, no side effects.
 
    .PARAMETER UpdateRing
        The operator's manual UpdateRing input. May be a single ring
        ('Ring0'), a ';'-joined list ('Ring0;Ring1'), the '***' / '*' wildcard,
        or $null / empty. Whitespace around tokens is trimmed.
 
    .PARAMETER Schedule
        The typed config object returned by Get-AzLocalApplyUpdatesScheduleConfig
        (exposes .AllowedUpdateVersions [string[]] and .Schedule rows each with
        .rings and .AllowedUpdateVersionsParsed).
 
    .OUTPUTS
        [PSCustomObject] with:
          EffectiveAllowList [string[]] cleaned allow-list entries to
                                                apply (empty when Source='None'
                                                or when the list is the 'Latest'
                                                no-constraint sentinel)
          AllowedUpdateVersionsValue [string] the ';'-joined string form of
                                                EffectiveAllowList (empty string
                                                when no constraint) - ready to
                                                assign to the resolver's
                                                RESOLVED_ALLOWED_UPDATE_VERSIONS
                                                step output
          Source 'RowOverride' | 'TopLevel' | 'None'
          IsLatest [bool] effective list is the 'Latest'
                                                no-constraint sentinel
    #>

    ########################################
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [AllowEmptyString()]
        [string]$UpdateRing,

        [Parameter(Mandatory = $true)]
        [psobject]$Schedule
    )

    Set-StrictMode -Version Latest

    # Parse the manual UpdateRing input into target ring tokens.
    $targetRings = @()
    if ($UpdateRing) {
        $targetRings = @(([string]$UpdateRing) -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }
    $wantsAll = (@($targetRings | Where-Object { $_ -eq '***' -or $_ -eq '*' }).Count -gt 0)

    # Top-level (fleet-wide global) default, cleaned of empty / whitespace tokens.
    $topAllow = @()
    if ($Schedule.PSObject.Properties['AllowedUpdateVersions'] -and $Schedule.AllowedUpdateVersions) {
        $topAllow = @($Schedule.AllowedUpdateVersions | Where-Object {
            -not [string]::IsNullOrWhiteSpace([string]$_)
        })
    }

    # Union of per-ring overrides across every row that covers a target ring.
    $rowUnion = New-Object 'System.Collections.Generic.List[string]'
    $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    if ($Schedule.PSObject.Properties['Schedule'] -and $Schedule.Schedule) {
        foreach ($row in @($Schedule.Schedule)) {
            if (-not $row.PSObject.Properties['rings'] -or [string]::IsNullOrWhiteSpace([string]$row.rings)) {
                continue
            }
            $ringTokens = @(([string]$row.rings) -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
            $rowIsWildcard = ($ringTokens -contains '***')
            $covers = $wantsAll -or $rowIsWildcard -or (@($ringTokens | Where-Object {
                $tok = $_
                @($targetRings | Where-Object {
                    [string]::Equals($_, $tok, [System.StringComparison]::OrdinalIgnoreCase)
                }).Count -gt 0
            }).Count -gt 0)
            if (-not $covers) { continue }

            if ($row.PSObject.Properties['AllowedUpdateVersionsParsed'] -and $row.AllowedUpdateVersionsParsed) {
                foreach ($v in @($row.AllowedUpdateVersionsParsed)) {
                    $sv = [string]$v
                    if (-not [string]::IsNullOrWhiteSpace($sv) -and $seen.Add($sv.Trim())) {
                        $rowUnion.Add($sv.Trim()) | Out-Null
                    }
                }
            }
        }
    }

    if ($rowUnion.Count -gt 0) {
        $effective = $rowUnion.ToArray()
        $source = 'RowOverride'
    }
    elseif ($topAllow.Count -gt 0) {
        $effective = @($topAllow)
        $source = 'TopLevel'
    }
    else {
        $effective = @()
        $source = 'None'
    }

    $isLatest = (@($effective).Count -gt 0) -and -not (@($effective | Where-Object {
        -not [string]::Equals([string]$_, 'Latest', [System.StringComparison]::OrdinalIgnoreCase)
    }).Count -gt 0)

    if ($isLatest) {
        $outList = @()
        $value = ''
    }
    else {
        $outList = @($effective)
        $value = ($outList -join ';')
    }

    return [PSCustomObject]@{
        EffectiveAllowList         = $outList
        AllowedUpdateVersionsValue = $value
        Source                     = $source
        IsLatest                   = $isLatest
    }
}