Private/Select-AzLocalNextUpdateForCluster.ps1

function Select-AzLocalNextUpdateForCluster {
    <#
    .SYNOPSIS
        Selects the next Azure Local solution update to install for a cluster,
        applying the optional AllowedUpdateVersions allow-list and the
        latest-by-YYMM auto-pick rule.
 
    .DESCRIPTION
        Pure decision helper extracted from Start-AzLocalClusterUpdate (v0.8.7)
        so the apply path AND the on-prem sideloading automation agree on which
        update is "next" for a given cluster. No side effects beyond the
        Get-LatestUpdateByYYMM warning - callers own all logging, CSV, and
        result-object emission.
 
        Selection rules (identical to the historic Start-AzLocalClusterUpdate
        behaviour):
          - If -UpdateName is supplied, that explicit choice wins. The named
            update must be present in -ReadyUpdates (matched on 'name'),
            otherwise Reason = 'UpdateNotFound'.
          - Otherwise, when -AllowedUpdateVersions is supplied and is NOT the
            'Latest' no-constraint sentinel, the Ready pool is filtered to
            updates whose 'name' OR 'properties.version' is an EXACT
            (case-insensitive) match for a supplied entry. An empty result =
            Reason 'NotInAllowList' (strict no-op, never falls back to latest).
          - 'Latest' (case-insensitive) appearing ALONE means "no constraint";
            the filter is skipped.
          - The winner is the latest Ready update by YYMM (Get-LatestUpdateByYYMM).
          - An empty -ReadyUpdates pool yields Reason 'NoneReady'.
 
    .PARAMETER ReadyUpdates
        The updates already filtered to a Ready state (Ready / ReadyToInstall).
 
    .PARAMETER AllowedUpdateVersions
        Optional allow-list of update names / version strings. 'Latest' (alone)
        disables filtering. Empty / whitespace entries are ignored.
 
    .PARAMETER UpdateName
        Optional explicit update name that overrides the allow-list.
 
    .OUTPUTS
        [PSCustomObject] with:
          Reason 'Selected' | 'NotInAllowList' | 'UpdateNotFound' | 'NoneReady'
          SelectedUpdate the chosen update object, or $null
          FilteredUpdates the Ready updates passing the allow-list (array)
          AllowOnlyLatest [bool] the allow-list was the 'Latest' sentinel
          AllowListEffective the cleaned non-empty allow-list entries (array)
          AllowDisplay comma-joined allow-list for messaging
          ReadyDisplay 'name (vVersion)' list of Ready updates for messaging
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [array]$ReadyUpdates,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [string[]]$AllowedUpdateVersions,

        [Parameter(Mandatory = $false)]
        [string]$UpdateName
    )

    $readyArr = @($ReadyUpdates | Where-Object { $null -ne $_ })
    $readyDisplay = (@($readyArr | ForEach-Object { "$($_.name) (v$($_.properties.version))" }) -join '; ')

    $allowListEffective = @($AllowedUpdateVersions | Where-Object {
        -not [string]::IsNullOrWhiteSpace([string]$_)
    })
    $allowDisplay = (@($allowListEffective) -join ', ')
    $allowOnlyLatest = ($allowListEffective.Count -gt 0) -and (
        -not (@($allowListEffective | Where-Object {
            -not [string]::Equals([string]$_, 'Latest', [System.StringComparison]::OrdinalIgnoreCase)
        }).Count -gt 0)
    )

    if ($readyArr.Count -eq 0) {
        return [PSCustomObject]@{
            Reason             = 'NoneReady'
            SelectedUpdate     = $null
            FilteredUpdates    = @()
            AllowOnlyLatest    = $allowOnlyLatest
            AllowListEffective = $allowListEffective
            AllowDisplay       = $allowDisplay
            ReadyDisplay       = $readyDisplay
        }
    }

    # Explicit -UpdateName wins over the allow-list.
    if ($UpdateName) {
        $named = @($readyArr | Where-Object { $_.name -eq $UpdateName })
        if ($named.Count -eq 0) {
            return [PSCustomObject]@{
                Reason             = 'UpdateNotFound'
                SelectedUpdate     = $null
                FilteredUpdates    = $readyArr
                AllowOnlyLatest    = $allowOnlyLatest
                AllowListEffective = $allowListEffective
                AllowDisplay       = $allowDisplay
                ReadyDisplay       = $readyDisplay
            }
        }
        return [PSCustomObject]@{
            Reason             = 'Selected'
            SelectedUpdate     = $named[0]
            FilteredUpdates    = $named
            AllowOnlyLatest    = $allowOnlyLatest
            AllowListEffective = $allowListEffective
            AllowDisplay       = $allowDisplay
            ReadyDisplay       = $readyDisplay
        }
    }

    # Allow-list filter (skipped when only 'Latest' or no list supplied).
    $pool = $readyArr
    if ($allowListEffective.Count -gt 0 -and -not $allowOnlyLatest) {
        $allowSet = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($a in $allowListEffective) {
            $sa = [string]$a
            if (-not [string]::IsNullOrWhiteSpace($sa)) { [void]$allowSet.Add($sa.Trim()) }
        }
        $filtered = @($readyArr | Where-Object {
            ($_.name -and $allowSet.Contains([string]$_.name)) -or
            ($_.properties -and $_.properties.version -and $allowSet.Contains([string]$_.properties.version))
        })
        if ($filtered.Count -eq 0) {
            return [PSCustomObject]@{
                Reason             = 'NotInAllowList'
                SelectedUpdate     = $null
                FilteredUpdates    = @()
                AllowOnlyLatest    = $allowOnlyLatest
                AllowListEffective = $allowListEffective
                AllowDisplay       = $allowDisplay
                ReadyDisplay       = $readyDisplay
            }
        }
        $pool = $filtered
    }

    $selected = Get-LatestUpdateByYYMM -Updates $pool
    return [PSCustomObject]@{
        Reason             = 'Selected'
        SelectedUpdate     = $selected
        FilteredUpdates    = $pool
        AllowOnlyLatest    = $allowOnlyLatest
        AllowListEffective = $allowListEffective
        AllowDisplay       = $allowDisplay
        ReadyDisplay       = $readyDisplay
    }
}