Public/Reset-AzureLocalSideloadedTag.ps1
|
function Reset-AzureLocalSideloadedTag { <# .SYNOPSIS Resets the UpdateSideloaded tag (True->False) and clears UpdateVersionInProgress on Azure Local clusters whose latest update run has succeeded. .DESCRIPTION Provides an explicit, scope-required entry point for the same auto-reset logic invoked by Get-AzureLocalUpdateRuns. Use this for: - Manual cleanup after an out-of-band update where Get-AzureLocalUpdateRuns was not run (or was run with -SkipSideloadedReset). - Forcing a reset (-Force) when an UpdateSideloaded=True tag is stuck because the operator abandoned the staged payload, or UpdateVersionInProgress is missing/mismatched. For each in-scope cluster the function fetches the latest update run, then applies the same decision matrix: UpdateSideloaded absent -> NoTag UpdateSideloaded=False -> Skipped (already reset) Latest run state != Succeeded -> RunNotSucceeded (preserved) UpdateSideloaded=True, no version -> Skipped (use -Force to override) UpdateSideloaded=True, mismatch -> Skipped (use -Force to override) UpdateSideloaded=True, match -> Reset -Force -> Reset (bypasses match check; still requires latest run state Succeeded) Scope must be explicit (no implicit -AllClusters): supply -ClusterNames, -ClusterResourceIds, or -ScopeByUpdateRingTag/-UpdateRingValue. .PARAMETER ClusterNames One or more cluster names to evaluate. .PARAMETER ClusterResourceIds One or more full ARM cluster resource IDs to evaluate. .PARAMETER ScopeByUpdateRingTag Selects clusters by an UpdateRing tag value via Azure Resource Graph. Must be paired with -UpdateRingValue. .PARAMETER UpdateRingValue The UpdateRing tag value to match when -ScopeByUpdateRingTag is used. .PARAMETER ResourceGroupName Optional - scopes -ClusterNames lookup to a single resource group. .PARAMETER SubscriptionId Optional - subscription context. Defaults to the current az subscription. .PARAMETER ApiVersion ARM api-version. Default is the module's default API version. .PARAMETER Force Bypasses the UpdateVersionInProgress match check. Still requires the cluster's latest run state to be 'Succeeded'. .OUTPUTS PSCustomObject[] - one row per cluster with ClusterName, Action, PreviousSideloaded, NewSideloaded, StagedVersion, MatchedRunUpdateName, Message. .EXAMPLE Reset-AzureLocalSideloadedTag -ClusterNames 'cl-01','cl-02' .EXAMPLE Reset-AzureLocalSideloadedTag -ScopeByUpdateRingTag -UpdateRingValue 'Wave1' .EXAMPLE # Force-clear stuck tag (operator abandoned the staged payload) Reset-AzureLocalSideloadedTag -ClusterNames 'cl-03' -Force -Confirm:$false .NOTES Requires az CLI authenticated with Microsoft.Resources/tags/read + Microsoft.Resources/tags/write on the cluster scope. No additional RBAC beyond what is already required by Set-AzureLocalClusterUpdateRingTag. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByName')] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory = $true, ParameterSetName = 'ByName')] [string[]]$ClusterNames, [Parameter(Mandatory = $true, ParameterSetName = 'ByResourceId')] [string[]]$ClusterResourceIds, [Parameter(Mandatory = $true, ParameterSetName = 'ByTag')] [switch]$ScopeByUpdateRingTag, [ValidatePattern('^[A-Za-z0-9_-]{1,64}$')] [Parameter(Mandatory = $true, ParameterSetName = 'ByTag')] [string]$UpdateRingValue, [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [string]$ResourceGroupName, [Parameter(Mandatory = $false)] [string]$SubscriptionId, [Parameter(Mandatory = $false)] [string]$ApiVersion = $script:DefaultApiVersion, [Parameter(Mandatory = $false)] [switch]$Force ) Test-AzCliAvailable | Out-Null if (-not $SubscriptionId) { $SubscriptionId = (az account show --query id -o tsv) } # Resolve in-scope clusters to {Name, ResourceId} $targets = @() switch ($PSCmdlet.ParameterSetName) { 'ByResourceId' { foreach ($rid in $ClusterResourceIds) { if ($rid -match '/clusters/([^/]+)$') { $targets += [PSCustomObject]@{ Name = $matches[1]; ResourceId = $rid } } } } 'ByName' { foreach ($name in $ClusterNames) { $info = Get-AzureLocalClusterInfo -ClusterName $name ` -ResourceGroupName $ResourceGroupName ` -SubscriptionId $SubscriptionId ` -ApiVersion $ApiVersion if ($info) { $targets += [PSCustomObject]@{ Name = $name; ResourceId = $info.id } } else { Write-Log -Message "Cluster '$name' not found - skipping." -Level Warning } } } 'ByTag' { $kqlQuery = @" resources | where type =~ 'microsoft.azurestackhci/clusters' | where tags['UpdateRing'] =~ '$UpdateRingValue' | project name, id "@ $rows = Invoke-AzResourceGraphQuery -Query $kqlQuery foreach ($row in $rows) { $targets += [PSCustomObject]@{ Name = $row.name; ResourceId = $row.id } } } } if ($targets.Count -eq 0) { Write-Log -Message "Reset-AzureLocalSideloadedTag: no matching clusters found." -Level Warning return @() } Write-Log -Message "Reset-AzureLocalSideloadedTag: evaluating $($targets.Count) cluster(s)..." -Level Info $results = New-Object System.Collections.Generic.List[object] foreach ($t in $targets) { # Fetch the latest update run state + name $latestRun = Get-AzLocalClusterUpdateRuns -resourceId $t.ResourceId -updateNameFilter $null -apiVer $ApiVersion | Sort-Object { $_.properties.timeStarted } -Descending | Select-Object -First 1 $state = '' $updName = '' if ($latestRun) { $state = [string]$latestRun.properties.state if ($latestRun.id -match '/updates/([^/]+)/updateRuns/') { $updName = $matches[1] } } # Honour -WhatIf / -Confirm. ShouldProcess gates the per-cluster # tag mutation; the underlying helper still no-ops on NoTag / NoRuns / # RunNotSucceeded states so this prompt only fires for clusters where # a tag write could actually occur. if (-not $PSCmdlet.ShouldProcess($t.Name, 'Reset UpdateSideloaded tag')) { Write-Log -Message "[$($t.Name)] Skipped (ShouldProcess declined)." -Level Info continue } $r = Invoke-AzLocalSideloadedAutoResetForCluster ` -ClusterName $t.Name ` -ClusterResourceId $t.ResourceId ` -LatestRunState $state ` -LatestRunUpdateName $updName ` -ApiVersion $ApiVersion ` -Force:$Force switch ($r.Action) { 'Reset' { Write-Log -Message "[$($t.Name)] $($r.Message)" -Level Success } 'OrphanCleared' { Write-Log -Message "[$($t.Name)] $($r.Message)" -Level Info } 'NoTag' { Write-Log -Message "[$($t.Name)] $($r.Message)" -Level Info } 'NoRuns' { Write-Log -Message "[$($t.Name)] $($r.Message)" -Level Info } 'RunNotSucceeded' { Write-Log -Message "[$($t.Name)] $($r.Message)" -Level Info } default { Write-Log -Message "[$($t.Name)] $($r.Message)" -Level Warning } } $results.Add($r) | Out-Null } return $results.ToArray() } |