Private/Get-AzLocalArmResource.ps1

Function Get-AzLocalArmResource {
    <#
    .SYNOPSIS
 
    Existence-aware wrapper around Get-AzResource that does not mask real errors.
 
    .DESCRIPTION
 
    Calling Get-AzResource for a resource ID with -ErrorAction SilentlyContinue
    silently swallows ALL errors - including HTTP 400 "unsupported api-version"
    and transport / auth / RBAC failures - and returns $null. Callers then
    misinterpret that $null as "the resource does not exist", producing misleading
    errors such as "Arc node not found" when the lookup actually failed for an
    unrelated reason.
 
    This wrapper distinguishes the three possible outcomes:
 
      * Resource exists -> returns the resource object.
      * Resource genuinely absent -> returns $null (HTTP 404 / ResourceNotFound).
      * Any other failure -> throws, surfacing the real ARM error message.
 
    It also self-heals the api-version negotiation problem. If ARM rejects the
    auto-negotiated api-version (HTTP 400 "No registered resource provider found
    ... The supported api-versions are ..."), it parses the supported versions
    from ARM's own error message and retries once with the newest stable
    (non-preview) version. This avoids hard-coding api-versions that age out.
 
    .PARAMETER ResourceId
 
    The full Azure resource ID to look up.
 
    .PARAMETER ResourceKind
 
    Friendly label used in verbose and error messages (e.g. 'Arc node', 'cluster').
    Defaults to 'resource'.
 
    .PARAMETER ApiVersion
 
    Optional explicit api-version for the FIRST lookup attempt (e.g. a resource
    type's known GA version). If ARM rejects it, the function self-heals by
    parsing the supported-versions list and retrying with the newest stable one.
 
    .OUTPUTS
 
    The resource object when it exists, or $null when it genuinely does not exist.
 
    .NOTES
    Author : Azure-Local DeploymentAutomation
    Purpose: Prevent Get-AzResource from masking real failures (api-version,
             auth, RBAC, transport) as "resource not found".
    #>


    [OutputType([object])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$ResourceId,

        [Parameter(Mandatory = $false, Position = 1)]
        [string]$ResourceKind = 'resource',

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

    # Error-message fragments that indicate the resource genuinely does not exist
    # (as opposed to a transport / validation / authorization failure).
    $notFoundPattern = 'ResourceNotFound|was not found|could not be found|StatusCode:\s*404'

    # Seed the first call with a caller-supplied api-version when provided (e.g. the
    # known GA version for the resource type). If ARM rejects it, the catch block
    # below self-heals by parsing ARM's supported-versions list and retrying.
    $getParams = @{ ResourceId = $ResourceId; ErrorAction = 'Stop' }
    if (-not [string]::IsNullOrWhiteSpace($ApiVersion)) { $getParams['ApiVersion'] = $ApiVersion }

    try {
        return Get-AzResource @getParams
    } catch {
        $msg = [string]$_.Exception.Message

        # Genuine absence -> this is an expected, non-error outcome.
        if ($msg -match $notFoundPattern) {
            Write-Verbose "$ResourceKind '$ResourceId' not found (genuine 404)."
            return $null
        }

        # Unsupported auto-negotiated api-version -> parse the supported versions
        # from ARM's error and retry once with the newest stable (non-preview) one.
        if ($msg -match 'No registered resource provider found' -or $msg -match 'supported api-versions') {
            $supportedSegment = $msg
            $idx = $msg.IndexOf('supported api-versions are')
            if ($idx -ge 0) { $supportedSegment = $msg.Substring($idx) }

            $versions = @([regex]::Matches($supportedSegment, '\d{4}-\d{2}-\d{2}(?:-preview)?') |
                ForEach-Object { $_.Value } | Select-Object -Unique)
            $stable = @($versions | Where-Object { $_ -notmatch 'preview' } | Sort-Object -Descending)
            $retryVersion = if ($stable.Count -gt 0) {
                $stable[0]
            } elseif ($versions.Count -gt 0) {
                (@($versions | Sort-Object -Descending))[0]
            } else {
                $null
            }

            if ($retryVersion) {
                Write-Verbose "Default api-version was rejected for $ResourceKind '$ResourceId'; retrying with supported api-version '$retryVersion'."
                try {
                    return Get-AzResource -ResourceId $ResourceId -ApiVersion $retryVersion -ErrorAction Stop
                } catch {
                    $retryMsg = [string]$_.Exception.Message
                    if ($retryMsg -match $notFoundPattern) {
                        Write-Verbose "$ResourceKind '$ResourceId' not found (genuine 404) at api-version '$retryVersion'."
                        return $null
                    }
                    throw "Failed to query $ResourceKind '$ResourceId' at api-version '$retryVersion': $retryMsg"
                }
            }
        }

        # Any other failure (auth, RBAC, throttling, transport) - surface the real
        # error instead of masking it as "not found".
        throw "Failed to query $ResourceKind '$ResourceId': $msg"
    }
}