@skyhook-io/k8s-ui 1.8.14 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/package.json +4 -1
- package/src/components/applications/ApplicationDetail.test.tsx +28 -0
- package/src/components/applications/ApplicationDetail.tsx +17 -0
- package/src/components/audit/AuditAlerts.tsx +13 -11
- package/src/components/audit/AuditBadgeTooltip.test.tsx +6 -0
- package/src/components/audit/AuditBadgeTooltip.tsx +9 -6
- package/src/components/audit/AuditCard.tsx +15 -13
- package/src/components/audit/AuditFindingsTable.tsx +33 -31
- package/src/components/audit/AuditSeverityPresentation.test.tsx +64 -0
- package/src/components/checks/types.ts +4 -4
- package/src/components/issues/types.ts +4 -0
- package/src/components/resources/ResourcesView.tsx +9 -7
- package/src/components/resources/get-pod-phase-display.test.ts +204 -0
- package/src/components/resources/resource-utils.ts +101 -0
- package/src/components/shared/CreateResourceDialog.tsx +307 -172
- package/src/components/shared/EditableYamlView.tsx +197 -26
- package/src/components/timeline/TimelineSwimlanes.tsx +28 -15
- package/src/components/timeline/index.ts +0 -2
- package/src/components/timeline/timeline-live.test.ts +0 -37
- package/src/components/timeline/timeline-live.ts +2 -24
- package/src/components/topology/K8sResourceNode.tsx +11 -9
- package/src/components/ui/FilterPill.tsx +3 -1
- package/src/components/ui/YamlEditor.test.ts +148 -0
- package/src/components/ui/YamlEditor.tsx +759 -186
- package/src/components/ui/YamlReview.test.ts +45 -0
- package/src/components/ui/YamlReview.tsx +426 -0
- package/src/components/ui/index.ts +9 -0
- package/src/components/ui/monacoRuntime.ts +41 -0
- package/src/components/ui/monacoYaml.worker.ts +1 -0
- package/src/components/ui/yamlMonacoRuntime.ts +110 -0
- package/src/components/workload/WorkloadView.tsx +27 -2
- package/src/monaco-deep.d.ts +5 -0
- package/src/types/core.ts +8 -0
- package/src/utils/yaml.test.ts +100 -2
- package/src/utils/yaml.ts +86 -1
|
@@ -133,13 +133,14 @@ import {
|
|
|
133
133
|
podMatchesProblemCategory,
|
|
134
134
|
SEVERITY_DOT_COLOR,
|
|
135
135
|
} from './resource-utils'
|
|
136
|
-
import { SEVERITY_BADGE, EVENT_TYPE_COLORS
|
|
136
|
+
import { SEVERITY_BADGE, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
|
|
137
137
|
import { pluralize } from '../../utils/pluralize'
|
|
138
138
|
import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
|
|
139
139
|
import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
|
|
140
140
|
import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
|
|
141
141
|
import { Tooltip } from '../ui/Tooltip'
|
|
142
142
|
import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
|
|
143
|
+
import { SEVERITY_TEXT_CLASS } from '../checks/severity'
|
|
143
144
|
// CRD-specific cell components (extracted)
|
|
144
145
|
import { GitRepositoryCell, OCIRepositoryCell, HelmRepositoryCell, KustomizationCell, FluxHelmReleaseCell, FluxAlertCell } from './renderers/flux-cells'
|
|
145
146
|
import { ArgoApplicationCell, ArgoApplicationSetCell, ArgoAppProjectCell } from './renderers/argo-cells'
|
|
@@ -1829,8 +1830,8 @@ interface ResourcesViewData {
|
|
|
1829
1830
|
onNavigate?: (path: string, options?: { replace?: boolean }) => void
|
|
1830
1831
|
certExpiry?: Record<string, { expired?: boolean; daysLeft: number }>
|
|
1831
1832
|
certExpiryError?: boolean
|
|
1832
|
-
|
|
1833
|
-
|
|
1833
|
+
/** Cluster Audit findings keyed by "namespace/name". Raw compatibility
|
|
1834
|
+
* counts render danger as High and warning as Medium. */
|
|
1834
1835
|
auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
|
|
1835
1836
|
onOpenLogs?: (params: { namespace: string; podName: string; containers: string[]; containerName?: string }) => void
|
|
1836
1837
|
onOpenWorkloadLogs?: (params: { namespace: string; workloadKind: string; workloadName: string }) => void
|
|
@@ -1889,7 +1890,8 @@ interface ResourcesViewProps {
|
|
|
1889
1890
|
topNodeMetrics?: TopNodeMetrics[]
|
|
1890
1891
|
certExpiry?: Record<string, { expired?: boolean; daysLeft: number }>
|
|
1891
1892
|
certExpiryError?: boolean
|
|
1892
|
-
|
|
1893
|
+
/** Cluster Audit findings keyed by "namespace/name". Raw compatibility
|
|
1894
|
+
* counts render danger as High and warning as Medium. */
|
|
1893
1895
|
auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
|
|
1894
1896
|
// Pinned kinds
|
|
1895
1897
|
pinned?: Array<{ name: string; kind: string; group: string }>
|
|
@@ -5381,8 +5383,8 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
|
|
|
5381
5383
|
{auditTotal > 0 && audit && (
|
|
5382
5384
|
<Tooltip content={audit.messages && audit.messages.length > 0
|
|
5383
5385
|
? <AuditBadgeTooltip messages={audit.messages} />
|
|
5384
|
-
: `${auditTotal} audit ${auditTotal === 1 ? 'finding' : 'findings'}${audit.danger > 0 ? ` · ${audit.danger}
|
|
5385
|
-
<span className={clsx('shrink-0 inline-flex items-center gap-0.5 text-[10px] font-medium cursor-help', audit.danger > 0 ?
|
|
5386
|
+
: `${auditTotal} audit ${auditTotal === 1 ? 'finding' : 'findings'}${audit.danger > 0 ? ` · ${audit.danger} high` : ''}${audit.warning > 0 ? ` · ${audit.warning} medium` : ''}`}>
|
|
5387
|
+
<span className={clsx('shrink-0 inline-flex items-center gap-0.5 text-[10px] font-medium cursor-help', audit.danger > 0 ? SEVERITY_TEXT_CLASS.high : SEVERITY_TEXT_CLASS.medium)}>
|
|
5386
5388
|
<AlertTriangle className="w-3 h-3" />
|
|
5387
5389
|
{auditTotal}
|
|
5388
5390
|
</span>
|
|
@@ -6265,7 +6267,7 @@ function ServiceCell({ resource, column }: { resource: any; column: string }) {
|
|
|
6265
6267
|
const flagged = auditBadges?.[`${meta.namespace || ''}/${meta.name}`]
|
|
6266
6268
|
if (flagged && flagged.danger + flagged.warning > 0) {
|
|
6267
6269
|
return (
|
|
6268
|
-
<span className={clsx('badge', flagged.danger > 0 ? 'status-
|
|
6270
|
+
<span className={clsx('badge', flagged.danger > 0 ? 'status-alert' : 'status-degraded')}>
|
|
6269
6271
|
No endpoints
|
|
6270
6272
|
</span>
|
|
6271
6273
|
)
|
|
@@ -26,6 +26,210 @@ describe('getPodStatus', () => {
|
|
|
26
26
|
}
|
|
27
27
|
expect(getPodStatus(crashing).level).toBe('unhealthy')
|
|
28
28
|
})
|
|
29
|
+
|
|
30
|
+
it('deranks a serving container with unsettled crash history', () => {
|
|
31
|
+
const recovered = {
|
|
32
|
+
status: {
|
|
33
|
+
phase: 'Running',
|
|
34
|
+
containerStatuses: [{
|
|
35
|
+
name: 'app',
|
|
36
|
+
ready: true,
|
|
37
|
+
restartCount: 2,
|
|
38
|
+
state: { running: {} },
|
|
39
|
+
lastState: { terminated: { reason: 'Error', exitCode: 1 } },
|
|
40
|
+
}],
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
expect(getPodStatus(recovered)).toMatchObject({ text: 'Restarted (2)', level: 'degraded' })
|
|
44
|
+
expect(getPodPhaseDisplay(recovered)).toMatchObject({
|
|
45
|
+
text: 'Running — Recently restarted (2 restarts)',
|
|
46
|
+
level: 'degraded',
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('keeps a non-serving container with crash history unhealthy', () => {
|
|
51
|
+
const down = {
|
|
52
|
+
status: {
|
|
53
|
+
phase: 'Running',
|
|
54
|
+
containerStatuses: [{
|
|
55
|
+
name: 'app',
|
|
56
|
+
ready: false,
|
|
57
|
+
restartCount: 2,
|
|
58
|
+
state: { running: {} },
|
|
59
|
+
lastState: { terminated: { reason: 'Error', exitCode: 1 } },
|
|
60
|
+
}],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
expect(getPodStatus(down).level).toBe('unhealthy')
|
|
64
|
+
expect(getPodPhaseDisplay(down).level).toBe('unhealthy')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('keeps a readiness-probed flapper visible until the settle window', () => {
|
|
68
|
+
const flapper = {
|
|
69
|
+
spec: { containers: [{ name: 'app', readinessProbe: { tcpSocket: { port: 8080 } } }] },
|
|
70
|
+
status: {
|
|
71
|
+
phase: 'Running',
|
|
72
|
+
containerStatuses: [{
|
|
73
|
+
name: 'app',
|
|
74
|
+
ready: true,
|
|
75
|
+
restartCount: 2,
|
|
76
|
+
state: { running: { startedAt: new Date(Date.now() - 60 * 1000).toISOString() } },
|
|
77
|
+
lastState: {
|
|
78
|
+
terminated: {
|
|
79
|
+
reason: 'Error',
|
|
80
|
+
exitCode: 1,
|
|
81
|
+
finishedAt: new Date(Date.now() - 60 * 1000).toISOString(),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}],
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
expect(getPodStatus(flapper).level).toBe('degraded')
|
|
88
|
+
|
|
89
|
+
const settled = structuredClone(flapper)
|
|
90
|
+
settled.status.containerStatuses[0].state.running.startedAt = new Date(Date.now() - 6 * 60 * 1000).toISOString()
|
|
91
|
+
expect(getPodStatus(settled).level).toBe('healthy')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('lets an active sibling failure outrank recovered crash history', () => {
|
|
95
|
+
const pod = {
|
|
96
|
+
status: {
|
|
97
|
+
phase: 'Running',
|
|
98
|
+
containerStatuses: [
|
|
99
|
+
{
|
|
100
|
+
name: 'app',
|
|
101
|
+
ready: true,
|
|
102
|
+
restartCount: 2,
|
|
103
|
+
state: { running: {} },
|
|
104
|
+
lastState: { terminated: { reason: 'Error', exitCode: 1 } },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'image',
|
|
108
|
+
ready: false,
|
|
109
|
+
state: { waiting: { reason: 'ImagePullBackOff' } },
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
expect(getPodStatus(pod)).toMatchObject({ text: 'ImagePullBackOff', level: 'unhealthy' })
|
|
115
|
+
expect(getPodPhaseDisplay(pod)).toMatchObject({
|
|
116
|
+
text: 'Running — ImagePullBackOff',
|
|
117
|
+
level: 'unhealthy',
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('keeps a not-ready sibling visible ahead of recovered crash history', () => {
|
|
122
|
+
const pod = {
|
|
123
|
+
status: {
|
|
124
|
+
phase: 'Running',
|
|
125
|
+
containerStatuses: [
|
|
126
|
+
{
|
|
127
|
+
name: 'app',
|
|
128
|
+
ready: true,
|
|
129
|
+
restartCount: 1,
|
|
130
|
+
state: { running: {} },
|
|
131
|
+
lastState: { terminated: { reason: 'Error', exitCode: 1 } },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'sidecar',
|
|
135
|
+
ready: false,
|
|
136
|
+
restartCount: 0,
|
|
137
|
+
state: { running: {} },
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
expect(getPodStatus(pod)).toMatchObject({ text: 'Running (1/2)', level: 'degraded' })
|
|
143
|
+
expect(getPodPhaseDisplay(pod)).toMatchObject({
|
|
144
|
+
text: 'Running — Not Ready (1/2)',
|
|
145
|
+
level: 'degraded',
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('counts only crash history still inside the settle window', () => {
|
|
150
|
+
const pod = {
|
|
151
|
+
status: {
|
|
152
|
+
phase: 'Running',
|
|
153
|
+
containerStatuses: [
|
|
154
|
+
{
|
|
155
|
+
name: 'app',
|
|
156
|
+
ready: true,
|
|
157
|
+
restartCount: 1,
|
|
158
|
+
state: { running: {} },
|
|
159
|
+
lastState: { terminated: { reason: 'Error', exitCode: 1 } },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'settled-sidecar',
|
|
163
|
+
ready: true,
|
|
164
|
+
restartCount: 40,
|
|
165
|
+
state: { running: { startedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString() } },
|
|
166
|
+
lastState: {
|
|
167
|
+
terminated: {
|
|
168
|
+
reason: 'Error',
|
|
169
|
+
exitCode: 1,
|
|
170
|
+
finishedAt: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
expect(getPodStatus(pod)).toMatchObject({ text: 'Restarted (1)', level: 'degraded' })
|
|
178
|
+
expect(getPodPhaseDisplay(pod)).toMatchObject({
|
|
179
|
+
text: 'Running — Recently restarted (1 restart)',
|
|
180
|
+
level: 'degraded',
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('does not revive old crash history after a long node outage', () => {
|
|
185
|
+
const pod = {
|
|
186
|
+
status: {
|
|
187
|
+
phase: 'Running',
|
|
188
|
+
containerStatuses: [{
|
|
189
|
+
name: 'app',
|
|
190
|
+
ready: true,
|
|
191
|
+
restartCount: 5,
|
|
192
|
+
state: { running: { startedAt: new Date(Date.now() - 30 * 1000).toISOString() } },
|
|
193
|
+
lastState: {
|
|
194
|
+
terminated: {
|
|
195
|
+
reason: 'Error',
|
|
196
|
+
exitCode: 1,
|
|
197
|
+
finishedAt: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
}],
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
expect(getPodStatus(pod)).toMatchObject({ text: 'Running', level: 'healthy' })
|
|
204
|
+
|
|
205
|
+
const maxBackoffContinuation = structuredClone(pod)
|
|
206
|
+
maxBackoffContinuation.status.containerStatuses[0].state.running.startedAt =
|
|
207
|
+
new Date(Date.now() - 60 * 1000).toISOString()
|
|
208
|
+
maxBackoffContinuation.status.containerStatuses[0].lastState.terminated.finishedAt =
|
|
209
|
+
new Date(Date.now() - 6 * 60 * 1000).toISOString()
|
|
210
|
+
expect(getPodStatus(maxBackoffContinuation).level).toBe('degraded')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('shows Terminating instead of a recovered-crash warning during graceful deletion', () => {
|
|
214
|
+
const terminating = {
|
|
215
|
+
metadata: { deletionTimestamp: new Date().toISOString() },
|
|
216
|
+
status: {
|
|
217
|
+
phase: 'Running',
|
|
218
|
+
containerStatuses: [{
|
|
219
|
+
name: 'app',
|
|
220
|
+
ready: true,
|
|
221
|
+
restartCount: 2,
|
|
222
|
+
state: { running: {} },
|
|
223
|
+
lastState: { terminated: { reason: 'Error', exitCode: 1 } },
|
|
224
|
+
}],
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
expect(getPodStatus(terminating)).toMatchObject({ text: 'Terminating', level: 'neutral' })
|
|
228
|
+
expect(getPodPhaseDisplay(terminating)).toMatchObject({
|
|
229
|
+
text: 'Running — Terminating',
|
|
230
|
+
level: 'neutral',
|
|
231
|
+
})
|
|
232
|
+
})
|
|
29
233
|
})
|
|
30
234
|
|
|
31
235
|
const podRunningHealthy = {
|
|
@@ -171,6 +171,63 @@ function firstFatalContainer(pod: any): { name: string; reason: string } | null
|
|
|
171
171
|
return null
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
const CRASH_SETTLE_MS = 5 * 60 * 1000
|
|
175
|
+
const STALE_CRASH_STATE_GAP_MS = 2 * CRASH_SETTLE_MS
|
|
176
|
+
|
|
177
|
+
function stableCrashHistory(cs: any): boolean {
|
|
178
|
+
if (!cs?.restartCount) return false
|
|
179
|
+
|
|
180
|
+
const startedAt = cs?.state?.running?.startedAt
|
|
181
|
+
const finishedAt = cs?.lastState?.terminated?.finishedAt
|
|
182
|
+
if (startedAt) {
|
|
183
|
+
const startedAtMs = new Date(startedAt).getTime()
|
|
184
|
+
if (Number.isFinite(startedAtMs) && Date.now() - startedAtMs >= CRASH_SETTLE_MS) return false
|
|
185
|
+
if (finishedAt) {
|
|
186
|
+
const finishedAtMs = new Date(finishedAt).getTime()
|
|
187
|
+
if (
|
|
188
|
+
Number.isFinite(startedAtMs)
|
|
189
|
+
&& Number.isFinite(finishedAtMs)
|
|
190
|
+
&& startedAtMs - finishedAtMs > STALE_CRASH_STATE_GAP_MS
|
|
191
|
+
) return false
|
|
192
|
+
}
|
|
193
|
+
} else if (cs?.state?.running && finishedAt) {
|
|
194
|
+
const finishedAtMs = new Date(finishedAt).getTime()
|
|
195
|
+
if (Number.isFinite(finishedAtMs) && Date.now() - finishedAtMs >= CRASH_SETTLE_MS) return false
|
|
196
|
+
}
|
|
197
|
+
if (cs?.state?.terminated?.exitCode === 0) return false
|
|
198
|
+
|
|
199
|
+
const term = cs?.lastState?.terminated
|
|
200
|
+
if (!term || term.reason === 'OOMKilled') return false
|
|
201
|
+
return term.reason === 'CrashLoopBackOff' || term.reason === 'Error' || Number(term.exitCode ?? 0) !== 0
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function podCrashHistoryLevel(pod: any): 'degraded' | 'unhealthy' | null {
|
|
205
|
+
let degraded = false
|
|
206
|
+
for (const cs of pod?.status?.containerStatuses || []) {
|
|
207
|
+
if (!stableCrashHistory(cs)) continue
|
|
208
|
+
if (!(cs?.state?.running && cs?.ready)) return 'unhealthy'
|
|
209
|
+
degraded = true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const initContainers = pod?.spec?.initContainers || []
|
|
213
|
+
for (const cs of pod?.status?.initContainerStatuses || []) {
|
|
214
|
+
const spec = initContainers.find((c: any) => c?.name === cs?.name)
|
|
215
|
+
const restartable = spec?.restartPolicy === 'Always'
|
|
216
|
+
if (!stableCrashHistory(cs)) continue
|
|
217
|
+
if (!(restartable && cs?.state?.running && cs?.ready)) return 'unhealthy'
|
|
218
|
+
degraded = true
|
|
219
|
+
}
|
|
220
|
+
return degraded ? 'degraded' : null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function podCrashRestartTotal(pod: any): number {
|
|
224
|
+
const main = pod?.status?.containerStatuses || []
|
|
225
|
+
const init = pod?.status?.initContainerStatuses || []
|
|
226
|
+
return [...main, ...init]
|
|
227
|
+
.filter(stableCrashHistory)
|
|
228
|
+
.reduce((sum, cs) => sum + (cs?.restartCount || 0), 0)
|
|
229
|
+
}
|
|
230
|
+
|
|
174
231
|
export function getPodStatus(pod: any): StatusBadge {
|
|
175
232
|
const phase = pod.status?.phase || 'Unknown'
|
|
176
233
|
const containerStatuses = pod.status?.containerStatuses || []
|
|
@@ -194,6 +251,17 @@ export function getPodStatus(pod: any): StatusBadge {
|
|
|
194
251
|
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
195
252
|
}
|
|
196
253
|
|
|
254
|
+
const crashHistoryLevel = phase !== 'Succeeded' && phase !== 'Unknown'
|
|
255
|
+
? podCrashHistoryLevel(pod)
|
|
256
|
+
: null
|
|
257
|
+
if (crashHistoryLevel === 'unhealthy') {
|
|
258
|
+
return {
|
|
259
|
+
text: 'CrashLoopBackOff',
|
|
260
|
+
color: healthColors.unhealthy,
|
|
261
|
+
level: 'unhealthy',
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
197
265
|
// Terminating: neutral while gracefully shutting down, degraded once stuck
|
|
198
266
|
// (a wedged finalizer / preStop hook holding the pod open).
|
|
199
267
|
if (pod.metadata?.deletionTimestamp) {
|
|
@@ -203,6 +271,16 @@ export function getPodStatus(pod: any): StatusBadge {
|
|
|
203
271
|
return { text: 'Terminating', color: healthColors.neutral, level: 'neutral' }
|
|
204
272
|
}
|
|
205
273
|
|
|
274
|
+
const hasUnsettledMainContainer = containerStatuses.some((c: any) => !containerSettledOk(c))
|
|
275
|
+
if (crashHistoryLevel === 'degraded' && !hasUnsettledMainContainer) {
|
|
276
|
+
const restarts = podCrashRestartTotal(pod)
|
|
277
|
+
return {
|
|
278
|
+
text: `Restarted (${restarts})`,
|
|
279
|
+
color: healthColors.degraded,
|
|
280
|
+
level: 'degraded',
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
206
284
|
switch (phase) {
|
|
207
285
|
case 'Running': {
|
|
208
286
|
// Degrade only on containers that are neither ready nor successfully done —
|
|
@@ -288,6 +366,18 @@ export function getPodPhaseDisplay(pod: any): PodPhaseDisplay {
|
|
|
288
366
|
return { phase, text: 'Failed', level: 'unhealthy' }
|
|
289
367
|
}
|
|
290
368
|
|
|
369
|
+
const crashHistoryLevel = phase !== 'Succeeded' && phase !== 'Unknown'
|
|
370
|
+
? podCrashHistoryLevel(pod)
|
|
371
|
+
: null
|
|
372
|
+
if (crashHistoryLevel === 'unhealthy') {
|
|
373
|
+
return {
|
|
374
|
+
phase,
|
|
375
|
+
text: `${phase} — CrashLoopBackOff`,
|
|
376
|
+
level: 'unhealthy',
|
|
377
|
+
hint: 'At least one container has crash history and is not serving now.',
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
291
381
|
// Terminating: neutral while gracefully shutting down, degraded once stuck.
|
|
292
382
|
if (pod?.metadata?.deletionTimestamp) {
|
|
293
383
|
const stuck = minutesSince(pod.metadata.deletionTimestamp) >= TERMINATING_STUCK_MINUTES
|
|
@@ -301,6 +391,17 @@ export function getPodPhaseDisplay(pod: any): PodPhaseDisplay {
|
|
|
301
391
|
}
|
|
302
392
|
}
|
|
303
393
|
|
|
394
|
+
const hasUnsettledMainContainer = containerStatuses.some((c) => !containerSettledOk(c))
|
|
395
|
+
if (crashHistoryLevel === 'degraded' && !hasUnsettledMainContainer) {
|
|
396
|
+
const crashRestarts = podCrashRestartTotal(pod)
|
|
397
|
+
return {
|
|
398
|
+
phase,
|
|
399
|
+
text: `${phase} — Recently restarted (${crashRestarts} restart${crashRestarts === 1 ? '' : 's'})`,
|
|
400
|
+
level: 'degraded',
|
|
401
|
+
hint: 'Containers are ready now but remain in the crash settle window.',
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
304
405
|
switch (phase) {
|
|
305
406
|
case 'Running': {
|
|
306
407
|
// A successfully-completed container (exit 0) is settled, not "not ready" —
|