Private/Format-AzLocalUpdateRun.ps1
|
function Format-AzLocalUpdateRun { [CmdletBinding()] [OutputType([PSCustomObject])] param($run, $clusterName = "", $clusterResourceId = "") $props = $run.properties # Resolve EndTime once via the central helper (used for both display and Duration fallback). $endTimeDt = Get-AzLocalRunEndTime -props $props $endTimeDisplay = if ($endTimeDt) { $endTimeDt.ToString("yyyy-MM-dd HH:mm") } else { "" } # Duration: prefer ARM-reported properties.duration (ISO-8601, e.g. "PT8H37M58S") # because it's authoritative and immune to clock skew. Fall back to # EndTime - StartTime, then to "running" for in-flight runs. $duration = "" $durationSpan = $null if ($props.PSObject.Properties['duration'] -and $props.duration) { try { $durationSpan = [System.Xml.XmlConvert]::ToTimeSpan([string]$props.duration) } catch { $null = $_ <# malformed ISO-8601 duration; fall through to EndTime-StartTime fallback #> } } if (-not $durationSpan -and $props.timeStarted -and $endTimeDt) { try { $durationSpan = $endTimeDt - [datetime]$props.timeStarted } catch { $null = $_ <# malformed timeStarted; leave duration blank #> } } if ($durationSpan) { $duration = Format-AzLocalDurationHuman -Value $durationSpan } elseif ($props.timeStarted -and $props.state -eq "InProgress") { try { $runningSpan = (Get-Date) - [datetime]$props.timeStarted $human = Format-AzLocalDurationHuman -Value $runningSpan if ($human) { $duration = "$human (running)" } } catch { $null = $_ <# malformed timeStarted on in-flight run; leave duration blank #> } } $currentStep = "" $currentStepDetail = "" $progress = "" $stepStartTimeDisplay = "" $stepElapsedDisplay = "" # Surfaced separately from State (v0.7.96) - exposes the portal 'Status' filter value. # Operationally critical: a run can sit at State=InProgress for days while progress.status # already shows 'Error', telling the operator a step has errored and the run is stuck. $progressStatus = if ($props.progress -and $props.progress.PSObject.Properties['status']) { [string]$props.progress.status } else { '' } $errorMessage = '' if ($props.progress -and $props.progress.steps) { $steps = $props.progress.steps # Wrap in @() so .Count returns 0 (not $null) when no step matches -- previously the # "completed" numerator rendered blank for runs that failed before any step succeeded. $completedSteps = @($steps | Where-Object { $_.status -eq "Success" }).Count $totalSteps = @($steps).Count $progress = "$completedSteps/$totalSteps steps" $inProgressStep = $steps | Where-Object { $_.status -eq "InProgress" } | Select-Object -First 1 $failedStep = $steps | Where-Object { $_.status -in @("Error", "Failed") } | Select-Object -First 1 if ($inProgressStep) { $currentStep = $inProgressStep.name } elseif ($failedStep) { $currentStep = "$($failedStep.name) (FAILED)" } $currentStepDetail = Get-CurrentStepPath -Steps $steps -IncludeErrorMessage if ([string]::IsNullOrWhiteSpace($currentStepDetail)) { $currentStepDetail = $currentStep } if ($currentStepDetail -match 'health check' -and $props.state -eq 'Failed') { if ($currentStepDetail -notmatch 'Critical health issues') { $currentStepDetail = "$currentStepDetail - Critical health issues must be resolved before updates can proceed" } } # Per-step elapsed (v0.7.96): walk to the deepest InProgress/Failed step and surface its # startTimeUtc + computed elapsed. This is the primary "is something stuck?" signal for # large-node clusters where overall-run duration alone is unreliable (a 16-node legitimate # run can easily exceed 8h while every individual step ticks along normally). $deepestActive = Get-DeepestActiveStep -Steps $steps # Deepest non-empty errorMessage from any Error/Failed step in the tree (v0.7.96). # Uses a coalesce(e8Msg..e1Msg) recursion so operators see the actual leaf failure # (e.g. CAU exception) rather than the generic parent step name. $errorMessage = Get-DeepestErrorMessage -Steps $steps if ($deepestActive -and $deepestActive.PSObject.Properties['startTimeUtc'] -and $deepestActive.startTimeUtc) { try { $stepStartDt = [datetime]::SpecifyKind(([datetime]$deepestActive.startTimeUtc).ToUniversalTime(), [DateTimeKind]::Utc) $stepStartTimeDisplay = $stepStartDt.ToString('yyyy-MM-dd HH:mm') $stepSpan = $null if ($props.state -eq 'InProgress' -and $deepestActive.status -eq 'InProgress') { $stepSpan = (Get-Date).ToUniversalTime() - $stepStartDt $human = Format-AzLocalDurationHuman -Value $stepSpan if ($human) { $stepElapsedDisplay = "$human (running)" } } elseif ($deepestActive.PSObject.Properties['endTimeUtc'] -and $deepestActive.endTimeUtc) { try { $stepEndDt = [datetime]::SpecifyKind(([datetime]$deepestActive.endTimeUtc).ToUniversalTime(), [DateTimeKind]::Utc) $stepSpan = $stepEndDt - $stepStartDt $human = Format-AzLocalDurationHuman -Value $stepSpan if ($human) { $stepElapsedDisplay = $human } } catch { $null = $_ <# malformed endTimeUtc on deepest step; leave step elapsed blank #> } } } catch { $null = $_ <# malformed startTimeUtc on deepest step; leave step start/elapsed blank #> } } } $updateNameExtracted = "" $runId = "" if ($run.id -match '/updates/([^/]+)/updateRuns/([^/]+)$') { $updateNameExtracted = $matches[1] $runId = $matches[2] } elseif ($run.name -match '/([^/]+)$') { $runId = $matches[1] } else { $runId = $run.name } $result = [PSCustomObject]@{ UpdateName = $updateNameExtracted RunId = $runId RunResourceId = if ($run.PSObject.Properties['id']) { [string]$run.id } else { '' } State = $props.state Status = $progressStatus StartTime = if ($props.timeStarted) { ([datetime]$props.timeStarted).ToString("yyyy-MM-dd HH:mm") } else { "" } EndTime = $endTimeDisplay Duration = $duration Progress = $progress CurrentStep = $currentStep CurrentStepDetail = $currentStepDetail StepStartTime = $stepStartTimeDisplay StepElapsed = $stepElapsedDisplay ErrorMessage = $errorMessage Location = $props.location } if ($clusterName) { $result | Add-Member -NotePropertyName "ClusterName" -NotePropertyValue $clusterName -Force } if ($clusterResourceId) { $result | Add-Member -NotePropertyName "ClusterResourceId" -NotePropertyValue $clusterResourceId -Force } return $result } |