Private/Test-AzLocalUpdateWindow.ps1

function Test-AzLocalUpdateWindow {
    <#
    .SYNOPSIS
        Tests whether a given time falls within a maintenance window.
    .DESCRIPTION
        Parses the UpdateStartWindow tag value and checks if the specified (or current) UTC time
        falls within any of the defined maintenance windows.
    .PARAMETER WindowString
        The UpdateStartWindow tag value to evaluate.
    .PARAMETER TestTime
        The UTC time to test against. Defaults to current UTC time.
    .PARAMETER AllowBeforeMinutes
        Minutes before the configured window opens that an update attempt is
        allowed to start. Default 0.
    .PARAMETER AllowAfterMinutes
        Minutes after the configured window closes that an update attempt is
        allowed to start. Default 0.
    .OUTPUTS
        PSCustomObject with Allowed (bool), Reason (string), MatchedWindow (string or $null)
    .EXAMPLE
        Test-AzLocalUpdateWindow -WindowString "Sat-Sun_02:00-06:00"
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$WindowString,

        [Parameter(Mandatory = $false)]
        [datetime]$TestTime = (Get-Date).ToUniversalTime(),

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 60)]
        [int]$AllowBeforeMinutes = 0,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 60)]
        [int]$AllowAfterMinutes = 0
    )

    # Maintenance windows are evaluated in UTC. If caller accidentally supplies
    # a Local or Unspecified DateTime, convert to UTC to avoid silently picking
    # the wrong hour/day (cluster update runs in the wrong window).
    if ($TestTime.Kind -ne [System.DateTimeKind]::Utc) {
        Write-Verbose "Test-AzLocalUpdateWindow: TestTime kind '$($TestTime.Kind)' converted to UTC."
        $TestTime = $TestTime.ToUniversalTime()
    }

    $windows = ConvertFrom-AzLocalUpdateWindow -WindowString $WindowString

    foreach ($window in $windows) {
        # Evaluate the previous, current, and next UTC dates as possible window
        # opening dates. The next date is required when a before allowance crosses
        # midnight; the previous date covers overnight and after allowances.
        foreach ($openingDate in @($TestTime.Date.AddDays(-1), $TestTime.Date, $TestTime.Date.AddDays(1))) {
            if ($openingDate.DayOfWeek -notin $window.Days) {
                continue
            }

            $windowStart = $openingDate.Add($window.StartTime)
            $windowEnd = if ($window.Overnight) {
                $openingDate.AddDays(1).Add($window.EndTime)
            }
            else {
                $openingDate.Add($window.EndTime)
            }
            $effectiveStart = $windowStart.AddMinutes(-$AllowBeforeMinutes)
            $effectiveEnd = $windowEnd.AddMinutes($AllowAfterMinutes)

            if ($TestTime -ge $effectiveStart -and $TestTime -lt $effectiveEnd) {
                $allowanceDetail = if ($AllowBeforeMinutes -gt 0 -or $AllowAfterMinutes -gt 0) {
                    " (allowance: $AllowBeforeMinutes minute(s) before, $AllowAfterMinutes minute(s) after)"
                }
                else {
                    ''
                }
                return [PSCustomObject]@{
                    Allowed       = $true
                    Reason        = "Within maintenance window: $($window.Raw)$allowanceDetail"
                    MatchedWindow = $window.Raw
                }
            }
        }
    }

    # No window matched
    $dayNames = ($windows | ForEach-Object { $_.Raw }) -join '; '
    return [PSCustomObject]@{
        Allowed       = $false
        Reason        = "Current time ($(($TestTime).ToString('yyyy-MM-dd HH:mm')) UTC, $($TestTime.DayOfWeek)) is outside all maintenance windows: $dayNames (allowance: $AllowBeforeMinutes minute(s) before, $AllowAfterMinutes minute(s) after)"
        MatchedWindow = $null
    }
}