Public/Get-InPlaceUpgradeCandidate.ps1

function Get-InPlaceUpgradeCandidate {
    <#
    .SYNOPSIS
    Discover Azure VMs that are tagged for an in-place upgrade

    .DESCRIPTION
    Lists the VMs in the current Azure context (optionally narrowed to a resource group or a single
    VM) and returns those whose tags select them: an UpgradeTarget tag is required, and the
    UpgradeState, UpgradeTarget and UpgradeRing filters must match when given. The default state
    filter is Pending, which is the state a human sets to approve an upgrade; the orchestrator
    passes UpgradeStarted for its Check mode. Tag comparison is case-insensitive and ignores
    surrounding whitespace. Read-only.

    .PARAMETER ResourceGroupName
    Restrict discovery to one resource group.

    .PARAMETER Name
    Restrict discovery to one VM. Requires -ResourceGroupName. The fast way to pilot on a single
    machine.

    .PARAMETER Target
    Only return VMs whose UpgradeTarget tag equals this value.

    .PARAMETER State
    Only return VMs whose UpgradeState tag equals this value. Pass an empty string to accept any
    state.

    .PARAMETER Ring
    Only return VMs whose UpgradeRing tag equals this value.

    .PARAMETER IgnoreTags
    Return the VM regardless of its tags. Only allowed together with -Name, so that a typo can
    never select a whole subscription.

    .EXAMPLE
    # Everything approved for an upgrade in the current subscription
    Get-InPlaceUpgradeCandidate | Select-Object Name, ResourceGroupName, @{ n = 'Target'; e = { $_.Tags['UpgradeTarget'] } }

    .EXAMPLE
    # Ring0 VMs in one resource group, then run the preflight on each
    Get-InPlaceUpgradeCandidate -ResourceGroupName rg-apps-prod-weu -Ring Ring0 | Test-InPlaceUpgradeReadiness

    .EXAMPLE
    # One untagged pilot VM
    Get-InPlaceUpgradeCandidate -ResourceGroupName rg-ipu-lab-weu -Name vm-ipu-2022-01 -IgnoreTags

    .INPUTS
    None

    .OUTPUTS
    Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine

    .NOTES
    Author: Simon Vedder (simonvedder.com)
    Version: 0.1.0
    Created: 2026-09-04
    LastModified: 2026-09-04
    RequiredPermissions: Microsoft.Compute/virtualMachines/read on the scope (the built-in Reader role covers it)
    Prerequisites: PowerShell 7.2+, Az.Compute; an established Azure context (Connect-AzAccount)

    .LINK
    https://github.com/simon-vedder/azure-vm-inplace-upgrade
    #>

    [CmdletBinding()]
    [OutputType('Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine')]
    param(
        [Parameter()]
        [string]$ResourceGroupName,

        [Parameter()]
        [string]$Name,

        [Parameter()]
        [string]$Target,

        [Parameter()]
        [AllowEmptyString()]
        [string]$State = 'Pending',

        [Parameter()]
        [string]$Ring,

        [Parameter()]
        [switch]$IgnoreTags
    )

    if ($IgnoreTags -and -not $Name) {
        throw '-IgnoreTags is only allowed together with -Name, so that it can never select every VM in scope.'
    }
    if ($Name -and -not $ResourceGroupName) {
        throw '-Name requires -ResourceGroupName.'
    }

    # The outer @() is not decoration: output of an if statement is enumerated on assignment, so
    # a single VM would arrive as a scalar and '.Count' below would throw under strict mode.
    $vms = @(
        if ($Name) {
            Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name -ErrorAction Stop
        }
        elseif ($ResourceGroupName) {
            Get-AzVM -ResourceGroupName $ResourceGroupName -ErrorAction Stop
        }
        else {
            Get-AzVM -ErrorAction Stop
        }
    )
    Write-Verbose "Discovered $($vms.Count) VM(s) in scope."

    foreach ($vm in $vms) {
        if ($IgnoreTags) {
            Write-Verbose "[$($vm.Name)] Tag filter bypassed (-IgnoreTags)."
            $vm
            continue
        }
        if (Test-CandidateTag -VM $vm -Target $Target -State $State -Ring $Ring) {
            Write-Verbose "[$($vm.Name)] Matches the upgrade tag selector."
            $vm
        }
    }
}