Private/ConvertTo-AzLocalUpdateRingKqlFilter.ps1

function ConvertTo-AzLocalUpdateRingKqlFilter {
    <#
    .SYNOPSIS
        Builds a KQL filter clause for an UpdateRing tag value that may be a
        single ring, multiple rings separated by ';', or '***' (match every
        cluster that has a non-empty UpdateRing tag).
    .DESCRIPTION
        v0.7.66 introduced multi-value UpdateRing inputs across all pipelines
        and cmdlets. Callers used to splice a single string into a KQL query
        like:
 
            | where tags['UpdateRing'] =~ '$UpdateRingValue'
 
        This helper accepts the relaxed input forms and returns the
        appropriate clause:
 
          - '***' => "| where isnotempty(tags['UpdateRing'])"
                                (matches every cluster that HAS the tag set
                                 to a non-empty value; clusters with no
                                 UpdateRing tag are deliberately excluded so
                                 untagged clusters stay opted-out)
          - 'Wave1' => "| where tags['UpdateRing'] =~ 'Wave1'"
          - 'Prod;Ring2' => "| where tags['UpdateRing'] in~ ('Prod','Ring2')"
 
        SAFETY: the wildcard token is three stars ('***'), not one. A single
        '*' is rejected by the upstream [ValidatePattern] on every public
        cmdlet so operators cannot accidentally scope a fleet-wide write
        (Start-AzLocalClusterUpdate, Set-AzLocalClusterUpdateRingTag,
        Reset-AzLocalSideloadedTag, Invoke-AzLocalFleetOperation) by
        typo. Three keystrokes is a deliberate gesture.
 
        Empty / whitespace segments produced by split are discarded.
        Embedded single quotes are doubled (KQL string-literal escaping) to
        keep the query injection-safe.
    .PARAMETER UpdateRingValue
        The raw UpdateRing value as it arrived from a pipeline parameter or
        cmdlet argument.
    .PARAMETER TagAccessor
        The KQL expression used to read the UpdateRing tag. Defaults to
        "tags['UpdateRing']" which matches direct cluster ARG queries. For
        the fleet health failures path (which goes via updateSummaries -> ARG
        hop on clusters) callers pass "tostring(tags['UpdateRing'])".
    .OUTPUTS
        [string] - the KQL where-clause, INCLUDING the leading '| where' if
        any filter is needed. Returns an empty string only when the input
        itself is null/empty/whitespace (i.e. when no scoping was requested
        at all - which the upstream Mandatory parameter binders normally
        prevent).
    .EXAMPLE
        $clause = ConvertTo-AzLocalUpdateRingKqlFilter -UpdateRingValue 'Prod;Ring2'
        # | where tags['UpdateRing'] in~ ('Prod','Ring2')
    .EXAMPLE
        $clause = ConvertTo-AzLocalUpdateRingKqlFilter -UpdateRingValue '***'
        # | where isnotempty(tags['UpdateRing'])
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$UpdateRingValue,

        [Parameter(Mandatory = $false)]
        [string]$TagAccessor = "tags['UpdateRing']"
    )

    if ([string]::IsNullOrWhiteSpace($UpdateRingValue)) { return '' }

    # '***' (three stars, exactly) is the deliberate fleet-wide token.
    # Single '*' and partial-star forms ('**', '****', '*Wave1') are blocked
    # by [ValidatePattern] on the callers, so they never reach this helper -
    # but we still check exactly '***' here to keep the contract explicit.
    if ($UpdateRingValue.Trim() -eq '***') {
        return "| where isnotempty($TagAccessor)"
    }

    $rings = @($UpdateRingValue -split ';' |
        ForEach-Object { $_.Trim() } |
        Where-Object { $_ })

    if (-not $rings -or $rings.Count -eq 0) { return '' }

    # @(...) wraps the pipeline output so we always get an array, even when the
    # caller passed a single ring. Without this, $escaped becomes a bare string
    # and $escaped[0] returns the FIRST CHARACTER ("'") rather than the first
    # element ("'Wave1'"), which silently corrupts the KQL where-clause.
    $escaped = @($rings | ForEach-Object {
        # KQL single-quote escaping: double each embedded quote, then wrap.
        $doubled = $_ -replace "'", "''"
        "'$doubled'"
    })

    if ($escaped.Count -eq 1) {
        return "| where $TagAccessor =~ $($escaped[0])"
    }

    return "| where $TagAccessor in~ ($($escaped -join ','))"
}