Private/Invoke-AzLocalSideloadedAutoResetForCluster.ps1
|
function Invoke-AzLocalSideloadedAutoResetForCluster { <# .SYNOPSIS Evaluates and (when matched) flips UpdateSideloaded=False + clears UpdateVersionInProgress for one cluster. .DESCRIPTION Implements the auto-reset decision matrix used by Get-AzureLocalUpdateRuns (default-on) and Reset-AzureLocalSideloadedTag (explicit). Returns a single PSCustomObject describing the action taken or the reason it was skipped. Decision matrix (LatestRunState=Succeeded only - any other state -> Skipped/RunNotSucceeded): UpdateSideloaded absent, no version -> NoTag (cluster opted out; nothing to do) UpdateSideloaded absent, orphan ver -> OrphanCleared (clear stale UpdateVersionInProgress only) UpdateSideloaded=False -> Skipped (already reset) UpdateSideloaded=True, no version -> Skipped (warning: no UpdateVersionInProgress) UpdateSideloaded=True, mismatch -> Skipped (mismatch reason) UpdateSideloaded=True, match -> Reset (PATCH both tags) UpdateSideloaded=True, -Force -> Reset (bypass match check) UpdateSideloaded with malformed value is treated as Skipped (with reason) so a typo cannot cause a silent reset. Orphan cleanup: if a cluster was previously updated through this module and then the operator removed the UpdateSideloaded tag (opting out of the workflow), the UpdateVersionInProgress tag would otherwise linger forever. When the latest run is Succeeded AND its name matches that tag, we clear it on a best-effort basis. We never write UpdateSideloaded in this path - the operator has explicitly opted out, and we only clean up our own breadcrumb. .PARAMETER ClusterName Display name of the cluster (for logging/output only). .PARAMETER ClusterResourceId Full ARM resource ID of the cluster. .PARAMETER LatestRunState State of the cluster's most recent update run (e.g. 'Succeeded', 'InProgress', 'Failed'). .PARAMETER LatestRunUpdateName UpdateName of the cluster's most recent update run (used for match check). .PARAMETER ApiVersion ARM api-version for the cluster GET/PATCH. .PARAMETER Force When specified, bypasses the UpdateVersionInProgress match check and resets the tags as long as UpdateSideloaded=True and the latest run state is Succeeded. .OUTPUTS PSCustomObject with ClusterName, Action (Reset|OrphanCleared|Skipped|NoTag|NoRuns|RunNotSucceeded), PreviousSideloaded, NewSideloaded, StagedVersion, MatchedRunUpdateName, Message. #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [string]$ClusterName, [Parameter(Mandatory = $true)] [string]$ClusterResourceId, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [string]$LatestRunState, [Parameter(Mandatory = $false)] [AllowNull()] [AllowEmptyString()] [string]$LatestRunUpdateName, [Parameter(Mandatory = $false)] [string]$ApiVersion = $script:DefaultApiVersion, [Parameter(Mandatory = $false)] [switch]$Force ) $result = [ordered]@{ ClusterName = $ClusterName Action = 'Skipped' PreviousSideloaded = $null NewSideloaded = $null StagedVersion = $null MatchedRunUpdateName = $LatestRunUpdateName Message = '' } # GET cluster to read current tags $getUri = "https://management.azure.com$ClusterResourceId`?api-version=$ApiVersion" $clusterJson = az rest --method GET --uri $getUri --only-show-errors 2>&1 if ($LASTEXITCODE -ne 0) { $result.Action = 'Skipped' $result.Message = "Failed to fetch cluster tags: $clusterJson" return [PSCustomObject]$result } $cluster = $clusterJson | ConvertFrom-Json $tagSideloaded = Get-TagValue -Tags $cluster.tags -Name $script:UpdateSideloadedTagName $tagVersion = Get-TagValue -Tags $cluster.tags -Name $script:UpdateVersionInProgressTagName $result.PreviousSideloaded = $tagSideloaded $result.StagedVersion = $tagVersion # 1. UpdateSideloaded tag absent if ([string]::IsNullOrWhiteSpace($tagSideloaded)) { # Orphan-cleanup branch: if there's a leftover UpdateVersionInProgress tag # (e.g. the cluster was updated via this module while opted-in, and the operator # has since removed UpdateSideloaded to opt out) and the latest run matches that # tag and succeeded, clear UpdateVersionInProgress on a best-effort basis. We do # NOT write UpdateSideloaded in this path - the cluster has explicitly opted out. if (-not [string]::IsNullOrWhiteSpace($tagVersion) ` -and $LatestRunState -eq 'Succeeded' ` -and (Test-AzLocalUpdateVersionInProgressMatch -TagValue $tagVersion -RunUpdateName $LatestRunUpdateName)) { if (-not $PSCmdlet.ShouldProcess($ClusterResourceId, "Clear orphan UpdateVersionInProgress (UpdateSideloaded tag absent)")) { $result.Action = 'NoTag' $result.Message = "WhatIf: would clear orphan UpdateVersionInProgress='$tagVersion'." return [PSCustomObject]$result } try { [void](Set-AzLocalClusterTagsMerge ` -ClusterResourceId $ClusterResourceId ` -Tags @{ $script:UpdateVersionInProgressTagName = $null } ` -ApiVersion $ApiVersion) $result.Action = 'OrphanCleared' $result.Message = "UpdateSideloaded tag absent; cleared orphan UpdateVersionInProgress='$tagVersion' (latest run '$LatestRunUpdateName' Succeeded)." } catch { $result.Action = 'NoTag' $result.Message = "UpdateSideloaded tag absent; failed to clear orphan UpdateVersionInProgress: $($_.Exception.Message)" } return [PSCustomObject]$result } $result.Action = 'NoTag' $result.Message = 'UpdateSideloaded tag not set; nothing to reset.' return [PSCustomObject]$result } # 2. Parse UpdateSideloaded - malformed -> skip (do not reset on malformed input) try { $sideloadedBool = ConvertFrom-AzLocalUpdateSideloaded -Value $tagSideloaded } catch { $result.Action = 'Skipped' $result.Message = "Malformed UpdateSideloaded tag '$tagSideloaded'; not resetting. ($($_.Exception.Message))" return [PSCustomObject]$result } # 3. Already False -> nothing to do if (-not $sideloadedBool) { $result.Action = 'Skipped' $result.Message = 'UpdateSideloaded=False already; no reset needed.' return [PSCustomObject]$result } # 4. Latest run must be Succeeded if ([string]::IsNullOrWhiteSpace($LatestRunState)) { # Distinct from "RunNotSucceeded" - cluster has no run history at all. # Surface as its own action so operators can tell "no runs yet" apart from # "latest run is InProgress / Failed". $result.Action = 'NoRuns' $result.Message = 'Cluster has no update runs yet; UpdateSideloaded preserved.' return [PSCustomObject]$result } if ($LatestRunState -ne 'Succeeded') { $result.Action = 'RunNotSucceeded' $result.Message = "Latest run state is '$LatestRunState'; UpdateSideloaded preserved (will be reset when a matching run succeeds)." return [PSCustomObject]$result } # 5. Match check (unless -Force) if (-not $Force) { if ([string]::IsNullOrWhiteSpace($tagVersion)) { $result.Action = 'Skipped' $result.Message = "UpdateSideloaded=True with no UpdateVersionInProgress tag (run started outside this module?). Skipping; use Reset-AzureLocalSideloadedTag -Force to override." return [PSCustomObject]$result } if (-not (Test-AzLocalUpdateVersionInProgressMatch -TagValue $tagVersion -RunUpdateName $LatestRunUpdateName)) { $result.Action = 'Skipped' $result.Message = "Latest succeeded run '$LatestRunUpdateName' does not match UpdateVersionInProgress '$tagVersion'; UpdateSideloaded preserved." return [PSCustomObject]$result } } # 6. Perform the flip $describe = if ($Force) { "force-reset (skipping version match)" } else { "matched version '$tagVersion'" } if (-not $PSCmdlet.ShouldProcess($ClusterResourceId, "Reset UpdateSideloaded=False, clear UpdateVersionInProgress ($describe)")) { $result.Action = 'Skipped' $result.Message = 'WhatIf: would reset UpdateSideloaded=False and clear UpdateVersionInProgress.' return [PSCustomObject]$result } try { [void](Set-AzLocalClusterTagsMerge ` -ClusterResourceId $ClusterResourceId ` -Tags @{ $script:UpdateSideloadedTagName = 'False' $script:UpdateVersionInProgressTagName = $null } ` -ApiVersion $ApiVersion) $result.Action = 'Reset' $result.NewSideloaded = 'False' $result.Message = if ($Force) { "UpdateSideloaded reset to False and UpdateVersionInProgress cleared (forced)." } else { "UpdateSideloaded reset to False and UpdateVersionInProgress cleared (matched run '$LatestRunUpdateName')." } } catch { $result.Action = 'Skipped' $result.Message = "Failed to PATCH tags: $($_.Exception.Message)" } return [PSCustomObject]$result } |