@skyhook-io/k8s-ui 1.4.0 → 1.4.2
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/package.json +2 -2
- package/src/components/resources/ResourcesSidebar.tsx +152 -57
- package/src/components/resources/ResourcesView.tsx +141 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +262 -45
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +7 -21
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +7 -56
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- package/src/components/shared/EditableYamlView.tsx +38 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +4 -4
- package/src/components/timeline/TimelineList.tsx +11 -4
- package/src/components/ui/Tooltip.tsx +16 -0
- package/src/components/ui/drawer-components.tsx +48 -1
- package/src/theme/variables.css +1 -1
- package/src/types/gitops.ts +8 -0
- package/src/utils/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Play, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
|
1
|
+
import { Play, Clock, CheckCircle, XCircle, Loader2, SkipForward, PauseCircle } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
4
|
import { formatAge, formatDuration } from '../resource-utils'
|
|
@@ -13,6 +13,8 @@ interface WorkflowStep {
|
|
|
13
13
|
phase: string
|
|
14
14
|
startedAt: string | null
|
|
15
15
|
finishedAt: string | null
|
|
16
|
+
message: string | null
|
|
17
|
+
nodeType: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function getStepDuration(step: WorkflowStep): string | null {
|
|
@@ -22,7 +24,13 @@ function getStepDuration(step: WorkflowStep): string | null {
|
|
|
22
24
|
return formatDuration(end.getTime() - start.getTime(), true)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
function StepStatusIcon({ phase }: { phase: string }) {
|
|
27
|
+
function StepStatusIcon({ phase, nodeType }: { phase: string; nodeType?: string }) {
|
|
28
|
+
if (nodeType === 'Skipped' || phase === 'Skipped') {
|
|
29
|
+
return <SkipForward className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
30
|
+
}
|
|
31
|
+
if (nodeType === 'Suspend' && phase !== 'Succeeded') {
|
|
32
|
+
return <PauseCircle className="w-4 h-4 text-yellow-400 shrink-0" />
|
|
33
|
+
}
|
|
26
34
|
switch (phase) {
|
|
27
35
|
case 'Succeeded':
|
|
28
36
|
return <CheckCircle className="w-4 h-4 text-green-400 shrink-0" />
|
|
@@ -46,6 +54,9 @@ function getPhaseBadgeClass(phase: string): string {
|
|
|
46
54
|
return 'status-unhealthy'
|
|
47
55
|
case 'Pending':
|
|
48
56
|
return 'status-degraded'
|
|
57
|
+
case 'Skipped':
|
|
58
|
+
case 'Omitted':
|
|
59
|
+
return 'status-unknown'
|
|
49
60
|
default:
|
|
50
61
|
return 'status-unknown'
|
|
51
62
|
}
|
|
@@ -74,29 +85,39 @@ function getWorkflowProblems(data: any): string[] {
|
|
|
74
85
|
problems.push(status.message || 'Workflow error')
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
// Check for failed nodes
|
|
88
|
+
// Check for failed nodes — include error messages when available
|
|
78
89
|
const nodes = status.nodes || {}
|
|
79
|
-
const
|
|
80
|
-
.filter((node: any) => node.type === 'Pod' && node.phase === 'Failed')
|
|
81
|
-
.map((node: any) => node.displayName)
|
|
90
|
+
const failedNodes = Object.values(nodes)
|
|
91
|
+
.filter((node: any) => (node.type === 'Pod' || node.type === 'Suspend' || node.type === 'Skipped') && (node.phase === 'Failed' || node.phase === 'Error'))
|
|
82
92
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
93
|
+
if (failedNodes.length > 0) {
|
|
94
|
+
const withMessages = failedNodes.filter((node: any) => node.message)
|
|
95
|
+
if (withMessages.length > 0) {
|
|
96
|
+
for (const n of withMessages as any[]) {
|
|
97
|
+
problems.push(`${n.displayName}: ${n.message}`)
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
problems.push(`Failed steps: ${failedNodes.map((n: any) => n.displayName).join(', ')}`)
|
|
101
|
+
}
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
return problems
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
const VISIBLE_NODE_TYPES = new Set(['Pod', 'Skipped', 'Suspend'])
|
|
108
|
+
|
|
90
109
|
function extractSteps(data: any): WorkflowStep[] {
|
|
91
110
|
const nodes = data.status?.nodes || {}
|
|
92
111
|
const steps: WorkflowStep[] = Object.entries(nodes)
|
|
93
|
-
.filter(([, node]: [string, any]) => node.type
|
|
112
|
+
.filter(([, node]: [string, any]) => VISIBLE_NODE_TYPES.has(node.type))
|
|
94
113
|
.map(([id, node]: [string, any]) => ({
|
|
95
114
|
id,
|
|
96
115
|
displayName: node.displayName || id,
|
|
97
|
-
phase: node.phase || 'Pending',
|
|
116
|
+
phase: node.phase || (node.type === 'Skipped' ? 'Skipped' : 'Pending'),
|
|
98
117
|
startedAt: node.startedAt || null,
|
|
99
118
|
finishedAt: node.finishedAt || null,
|
|
119
|
+
message: node.message || null,
|
|
120
|
+
nodeType: node.type,
|
|
100
121
|
}))
|
|
101
122
|
|
|
102
123
|
steps.sort((a, b) => {
|
|
@@ -163,9 +184,22 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
163
184
|
{status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
|
|
164
185
|
<Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
|
|
165
186
|
{templateName && <Property label="Template" value={templateName} />}
|
|
187
|
+
{status.progress && (
|
|
188
|
+
<Property label="Progress" value={status.progress} />
|
|
189
|
+
)}
|
|
166
190
|
{estimatedDuration != null && (
|
|
167
191
|
<Property label="Estimated Duration" value={formatEstimatedDuration(estimatedDuration)} />
|
|
168
192
|
)}
|
|
193
|
+
{status.resourcesDuration && (
|
|
194
|
+
<Property
|
|
195
|
+
label="Resource Usage"
|
|
196
|
+
value={
|
|
197
|
+
Object.entries(status.resourcesDuration as Record<string, number>)
|
|
198
|
+
.map(([resource, seconds]) => `${resource}: ${seconds}s`)
|
|
199
|
+
.join(', ')
|
|
200
|
+
}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
169
203
|
</PropertyList>
|
|
170
204
|
</Section>
|
|
171
205
|
|
|
@@ -173,13 +207,33 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
173
207
|
{steps.length > 0 && (
|
|
174
208
|
<Section title={`Steps (${steps.length})`} defaultExpanded>
|
|
175
209
|
<div className="space-y-1.5">
|
|
176
|
-
{steps.map(step =>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
210
|
+
{steps.map(step => {
|
|
211
|
+
const isFailed = step.phase === 'Failed' || step.phase === 'Error'
|
|
212
|
+
const isSkipped = step.nodeType === 'Skipped' || step.phase === 'Skipped'
|
|
213
|
+
const isSuspend = step.nodeType === 'Suspend'
|
|
214
|
+
return (
|
|
215
|
+
<div key={step.id} className={clsx(
|
|
216
|
+
'text-sm card-inner px-3 py-2',
|
|
217
|
+
isFailed && 'border-l-2 border-red-500'
|
|
218
|
+
)}>
|
|
219
|
+
<div className="flex items-center gap-2">
|
|
220
|
+
<StepStatusIcon phase={step.phase} nodeType={step.nodeType} />
|
|
221
|
+
<span className={clsx(
|
|
222
|
+
'flex-1',
|
|
223
|
+
isSkipped ? 'text-theme-text-tertiary' : isSuspend ? 'text-yellow-400' : 'text-theme-text-primary'
|
|
224
|
+
)}>
|
|
225
|
+
{step.displayName}
|
|
226
|
+
{isSkipped && <span className="ml-1 text-xs text-theme-text-tertiary">(skipped)</span>}
|
|
227
|
+
{isSuspend && <span className="ml-1 text-xs text-yellow-400/70">(suspend)</span>}
|
|
228
|
+
</span>
|
|
229
|
+
<span className="text-xs text-theme-text-secondary">{getStepDuration(step) || '-'}</span>
|
|
230
|
+
</div>
|
|
231
|
+
{isFailed && step.message && (
|
|
232
|
+
<div className="text-xs text-red-400 mt-1 ml-6 break-all">{step.message}</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
})}
|
|
183
237
|
</div>
|
|
184
238
|
</Section>
|
|
185
239
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Play, FileText, Key, Lock } from 'lucide-react'
|
|
1
|
+
import { Play, FileText, Key, Lock, File, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
3
3
|
|
|
4
4
|
interface WorkflowTemplateRendererProps {
|
|
@@ -44,6 +44,11 @@ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps
|
|
|
44
44
|
{templates.map((template: any) => {
|
|
45
45
|
const type = getTemplateType(template)
|
|
46
46
|
const image = getTemplateImage(template)
|
|
47
|
+
const inputParams: Array<{ name: string }> = template.inputs?.parameters || []
|
|
48
|
+
const outputParams: Array<{ name: string }> = template.outputs?.parameters || []
|
|
49
|
+
const outputArtifacts: Array<{ name: string }> = template.outputs?.artifacts || []
|
|
50
|
+
const inputArtifacts: Array<{ name: string }> = template.inputs?.artifacts || []
|
|
51
|
+
const hasIO = inputParams.length > 0 || outputParams.length > 0 || outputArtifacts.length > 0 || inputArtifacts.length > 0
|
|
47
52
|
return (
|
|
48
53
|
<div key={template.name} className="card-inner px-3 py-2 text-sm">
|
|
49
54
|
<div className="font-medium text-theme-text-primary">{template.name}</div>
|
|
@@ -53,6 +58,46 @@ export function WorkflowTemplateRenderer({ data }: WorkflowTemplateRendererProps
|
|
|
53
58
|
{image}
|
|
54
59
|
</div>
|
|
55
60
|
)}
|
|
61
|
+
{hasIO && (
|
|
62
|
+
<div className="mt-2 space-y-1.5">
|
|
63
|
+
{(inputParams.length > 0 || inputArtifacts.length > 0) && (
|
|
64
|
+
<div className="flex items-start gap-1.5">
|
|
65
|
+
<ArrowDownToLine className="w-3 h-3 text-theme-text-tertiary mt-0.5 shrink-0" />
|
|
66
|
+
<div className="flex flex-wrap gap-1">
|
|
67
|
+
{inputParams.map((p) => (
|
|
68
|
+
<span key={p.name} className="badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
69
|
+
{p.name}
|
|
70
|
+
</span>
|
|
71
|
+
))}
|
|
72
|
+
{inputArtifacts.map((a) => (
|
|
73
|
+
<span key={a.name} className="badge-sm bg-theme-elevated text-theme-text-secondary flex items-center gap-0.5">
|
|
74
|
+
<File className="w-2.5 h-2.5" />
|
|
75
|
+
{a.name}
|
|
76
|
+
</span>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{(outputParams.length > 0 || outputArtifacts.length > 0) && (
|
|
82
|
+
<div className="flex items-start gap-1.5">
|
|
83
|
+
<ArrowUpFromLine className="w-3 h-3 text-theme-text-tertiary mt-0.5 shrink-0" />
|
|
84
|
+
<div className="flex flex-wrap gap-1">
|
|
85
|
+
{outputParams.map((p) => (
|
|
86
|
+
<span key={p.name} className="badge-sm bg-theme-elevated text-theme-text-secondary">
|
|
87
|
+
{p.name}
|
|
88
|
+
</span>
|
|
89
|
+
))}
|
|
90
|
+
{outputArtifacts.map((a) => (
|
|
91
|
+
<span key={a.name} className="badge-sm bg-theme-elevated text-theme-text-secondary flex items-center gap-0.5">
|
|
92
|
+
<File className="w-2.5 h-2.5" />
|
|
93
|
+
{a.name}
|
|
94
|
+
</span>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
56
101
|
</div>
|
|
57
102
|
)
|
|
58
103
|
})}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Tooltip } from '../../ui/Tooltip'
|
|
5
5
|
import {
|
|
6
|
+
CAPACITY_TYPE_BADGE,
|
|
6
7
|
getNodePoolStatus,
|
|
7
8
|
getNodePoolNodeClassRef,
|
|
8
9
|
getNodePoolLimits,
|
|
@@ -22,9 +23,11 @@ export function NodePoolCell({ resource, column }: { resource: any; column: stri
|
|
|
22
23
|
case 'status': {
|
|
23
24
|
const status = getNodePoolStatus(resource)
|
|
24
25
|
return (
|
|
25
|
-
<
|
|
26
|
-
{status.
|
|
27
|
-
|
|
26
|
+
<Tooltip content={status.text}>
|
|
27
|
+
<span className={clsx('badge truncate max-w-[140px]', status.color)}>
|
|
28
|
+
{status.text}
|
|
29
|
+
</span>
|
|
30
|
+
</Tooltip>
|
|
28
31
|
)
|
|
29
32
|
}
|
|
30
33
|
case 'nodeClass': {
|
|
@@ -70,6 +73,18 @@ export function NodeClaimCell({ resource, column }: { resource: any; column: str
|
|
|
70
73
|
const pool = getNodeClaimNodePoolRef(resource)
|
|
71
74
|
return <span className="text-sm text-theme-text-secondary">{pool}</span>
|
|
72
75
|
}
|
|
76
|
+
case 'capacityType': {
|
|
77
|
+
const ct = resource.metadata?.labels?.['karpenter.sh/capacity-type'] || '-'
|
|
78
|
+
return (
|
|
79
|
+
<span className={clsx('badge-sm', CAPACITY_TYPE_BADGE[ct] || '')}>
|
|
80
|
+
{ct}
|
|
81
|
+
</span>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
case 'zone': {
|
|
85
|
+
const zone = resource.metadata?.labels?.['topology.kubernetes.io/zone'] || '-'
|
|
86
|
+
return <span className="text-sm text-theme-text-secondary">{zone}</span>
|
|
87
|
+
}
|
|
73
88
|
default:
|
|
74
89
|
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
75
90
|
}
|
|
@@ -166,6 +166,43 @@ export function getCNPGClusterPostgresParams(resource: any): Record<string, stri
|
|
|
166
166
|
return resource.spec?.postgresql?.parameters || {}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/** Per-instance replication state from status.instancesReportedState */
|
|
170
|
+
export interface CNPGInstanceReportedState {
|
|
171
|
+
podName: string
|
|
172
|
+
isPrimary: boolean
|
|
173
|
+
timelineID?: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getCNPGClusterInstancesReportedState(resource: any): CNPGInstanceReportedState[] {
|
|
177
|
+
const reported = resource.status?.instancesReportedState
|
|
178
|
+
if (!reported || typeof reported !== 'object') return []
|
|
179
|
+
return Object.entries(reported).map(([podName, state]: [string, any]) => ({
|
|
180
|
+
podName,
|
|
181
|
+
isPrimary: state?.isPrimary === true,
|
|
182
|
+
timelineID: state?.timelineID,
|
|
183
|
+
}))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Certificate expirations from status.certificates.expirations */
|
|
187
|
+
export interface CNPGCertificateExpiry {
|
|
188
|
+
secretName: string
|
|
189
|
+
expiryDate: string
|
|
190
|
+
daysUntilExpiry: number
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getCNPGClusterCertificateExpirations(resource: any): CNPGCertificateExpiry[] {
|
|
194
|
+
const expirations = resource.status?.certificates?.expirations
|
|
195
|
+
if (!expirations || typeof expirations !== 'object') return []
|
|
196
|
+
const now = new Date()
|
|
197
|
+
return Object.entries(expirations).map(([secretName, expiryDate]: [string, any]) => {
|
|
198
|
+
const expiry = new Date(expiryDate)
|
|
199
|
+
const daysUntilExpiry = isNaN(expiry.getTime())
|
|
200
|
+
? -1 // treat unparseable dates as expired/critical
|
|
201
|
+
: Math.floor((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
|
202
|
+
return { secretName, expiryDate: String(expiryDate), daysUntilExpiry }
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
169
206
|
// ============================================================================
|
|
170
207
|
// CNPG BACKUP UTILITIES
|
|
171
208
|
// ============================================================================
|
|
@@ -342,3 +379,13 @@ export function getCNPGPoolerInstances(resource: any): string {
|
|
|
342
379
|
export function getCNPGPoolerParameters(resource: any): Record<string, string> {
|
|
343
380
|
return resource.spec?.pgbouncer?.parameters || {}
|
|
344
381
|
}
|
|
382
|
+
|
|
383
|
+
export function getCNPGPoolerAuthQuery(resource: any): string | undefined {
|
|
384
|
+
return resource.spec?.pgbouncer?.authQuery
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function getCNPGPoolerAuthQuerySecret(resource: any): { name: string } | undefined {
|
|
388
|
+
const secret = resource.spec?.pgbouncer?.authQuerySecret
|
|
389
|
+
if (!secret?.name) return undefined
|
|
390
|
+
return { name: secret.name }
|
|
391
|
+
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
import type { StatusBadge } from './resource-utils'
|
|
4
4
|
import { healthColors } from './resource-utils'
|
|
5
5
|
|
|
6
|
+
// Shared capacity type badge colors for NodeClaim renderer and table cells
|
|
7
|
+
export const CAPACITY_TYPE_BADGE: Record<string, string> = {
|
|
8
|
+
spot: 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-400 dark:border-amber-700/40',
|
|
9
|
+
'on-demand': 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40',
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
// ============================================================================
|
|
7
13
|
// KARPENTER NODEPOOL UTILITIES
|
|
8
14
|
// ============================================================================
|
|
@@ -99,23 +99,67 @@ export function getPrometheusRuleTotalRules(resource: any): number {
|
|
|
99
99
|
return groups.reduce((sum: number, g: any) => sum + (g.rules?.length || 0), 0)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export
|
|
102
|
+
export interface PrometheusAlertRule {
|
|
103
|
+
type: 'alert'
|
|
104
|
+
alert: string
|
|
105
|
+
expr: string
|
|
106
|
+
for?: string
|
|
107
|
+
severity?: string
|
|
108
|
+
summary?: string
|
|
109
|
+
description?: string
|
|
110
|
+
labels?: Record<string, string>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface PrometheusRecordingRule {
|
|
114
|
+
type: 'recording'
|
|
115
|
+
record: string
|
|
116
|
+
expr: string
|
|
117
|
+
labels?: Record<string, string>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type PrometheusRule = PrometheusAlertRule | PrometheusRecordingRule
|
|
121
|
+
|
|
122
|
+
export interface PrometheusRuleGroup {
|
|
103
123
|
name: string
|
|
104
124
|
interval?: string
|
|
105
125
|
ruleCount: number
|
|
106
126
|
alertCount: number
|
|
107
127
|
recordCount: number
|
|
108
|
-
|
|
128
|
+
rules: PrometheusRule[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getPrometheusRuleGroups(resource: any): PrometheusRuleGroup[] {
|
|
109
132
|
return (resource.spec?.groups || []).map((g: any) => {
|
|
110
|
-
const
|
|
111
|
-
const alertCount =
|
|
112
|
-
const recordCount =
|
|
133
|
+
const rawRules = g.rules || []
|
|
134
|
+
const alertCount = rawRules.filter((r: any) => r.alert).length
|
|
135
|
+
const recordCount = rawRules.filter((r: any) => r.record).length
|
|
136
|
+
const rules: PrometheusRule[] = rawRules.map((r: any) => {
|
|
137
|
+
if (r.alert) {
|
|
138
|
+
return {
|
|
139
|
+
type: 'alert' as const,
|
|
140
|
+
alert: r.alert,
|
|
141
|
+
expr: r.expr || '',
|
|
142
|
+
for: r.for,
|
|
143
|
+
severity: r.labels?.severity,
|
|
144
|
+
summary: r.annotations?.summary,
|
|
145
|
+
description: r.annotations?.description,
|
|
146
|
+
labels: r.labels,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
type: 'recording' as const,
|
|
151
|
+
record: r.record || '',
|
|
152
|
+
expr: r.expr || '',
|
|
153
|
+
labels: r.labels,
|
|
154
|
+
}
|
|
155
|
+
})
|
|
113
156
|
return {
|
|
114
157
|
name: g.name,
|
|
115
158
|
interval: g.interval,
|
|
116
|
-
ruleCount:
|
|
159
|
+
ruleCount: rawRules.length,
|
|
117
160
|
alertCount,
|
|
118
161
|
recordCount,
|
|
162
|
+
rules,
|
|
119
163
|
}
|
|
120
164
|
})
|
|
121
165
|
}
|
|
@@ -42,6 +42,44 @@ export interface PodProblem {
|
|
|
42
42
|
message: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Tailwind classes for severity dot indicators (used in tooltips and alert banners) */
|
|
46
|
+
export const SEVERITY_DOT_COLOR: Record<PodProblem['severity'], string> = {
|
|
47
|
+
critical: 'bg-red-400',
|
|
48
|
+
high: 'bg-orange-400',
|
|
49
|
+
medium: 'bg-yellow-400',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Check whether a pod's problems match a given problem category (filter chip label) */
|
|
53
|
+
export function podMatchesProblemCategory(problems: PodProblem[], restarts: number, category: string): boolean {
|
|
54
|
+
const msgs = problems.map(p => p.message)
|
|
55
|
+
switch (category) {
|
|
56
|
+
case 'CrashLoopBackOff':
|
|
57
|
+
return msgs.includes('CrashLoopBackOff')
|
|
58
|
+
case 'ImagePullBackOff':
|
|
59
|
+
return msgs.some(m => m.includes('ImagePull') && !m.startsWith('Init:'))
|
|
60
|
+
case 'OOMKilled':
|
|
61
|
+
return msgs.includes('OOMKilled')
|
|
62
|
+
case 'Unschedulable':
|
|
63
|
+
return msgs.includes('Unschedulable')
|
|
64
|
+
case 'Not Ready':
|
|
65
|
+
return msgs.includes('Not Ready') || msgs.some(m => m.includes('Probe'))
|
|
66
|
+
case 'High Restarts':
|
|
67
|
+
return restarts > 5
|
|
68
|
+
case 'Init Failed':
|
|
69
|
+
return msgs.some(m => m.startsWith('Init:'))
|
|
70
|
+
case 'Exit Code Error':
|
|
71
|
+
return msgs.some(m => m.startsWith('Exit Code'))
|
|
72
|
+
case 'Failed':
|
|
73
|
+
return msgs.includes('Failed') || msgs.includes('Unknown')
|
|
74
|
+
case 'Other': {
|
|
75
|
+
const knownPatterns = ['CrashLoopBackOff', 'ImagePull', 'ErrImagePull', 'OOMKilled', 'Unschedulable', 'Not Ready', 'Probe', 'Init:', 'Exit Code', 'Failed', 'Unknown']
|
|
76
|
+
return problems.some(p => !knownPatterns.some(pat => p.message.includes(pat)) && p.message !== `${restarts} restarts`)
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
45
83
|
export function getPodStatus(pod: any): StatusBadge {
|
|
46
84
|
const phase = pod.status?.phase || 'Unknown'
|
|
47
85
|
const containerStatuses = pod.status?.containerStatuses || []
|
|
@@ -87,7 +125,31 @@ export function getPodStatus(pod: any): StatusBadge {
|
|
|
87
125
|
export function getPodProblems(pod: any): PodProblem[] {
|
|
88
126
|
const problems: PodProblem[] = []
|
|
89
127
|
const containerStatuses = pod.status?.containerStatuses || []
|
|
128
|
+
const initContainerStatuses = pod.status?.initContainerStatuses || []
|
|
90
129
|
const conditions = pod.status?.conditions || []
|
|
130
|
+
const phase = pod.status?.phase
|
|
131
|
+
|
|
132
|
+
// Failed or Unknown phase
|
|
133
|
+
if (phase === 'Failed' && pod.status?.reason !== 'Evicted') {
|
|
134
|
+
problems.push({ severity: 'critical', message: 'Failed' })
|
|
135
|
+
} else if (phase === 'Unknown') {
|
|
136
|
+
problems.push({ severity: 'high', message: 'Unknown' })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Init container failures
|
|
140
|
+
for (const cs of initContainerStatuses) {
|
|
141
|
+
if (cs.state?.waiting?.reason && cs.state.waiting.reason !== 'PodInitializing') {
|
|
142
|
+
const reason = cs.state.waiting.reason
|
|
143
|
+
if (['CrashLoopBackOff', 'ImagePullBackOff', 'ErrImagePull'].includes(reason)) {
|
|
144
|
+
problems.push({ severity: 'critical', message: `Init: ${reason}` })
|
|
145
|
+
} else {
|
|
146
|
+
problems.push({ severity: 'high', message: `Init: ${reason}` })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (cs.state?.terminated?.exitCode && cs.state.terminated.exitCode !== 0) {
|
|
150
|
+
problems.push({ severity: 'high', message: `Init: Exit Code ${cs.state.terminated.exitCode}` })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
91
153
|
|
|
92
154
|
for (const cs of containerStatuses) {
|
|
93
155
|
// Check waiting state
|
|
@@ -99,11 +161,15 @@ export function getPodProblems(pod: any): PodProblem[] {
|
|
|
99
161
|
problems.push({ severity: 'critical', message: 'Config Error' })
|
|
100
162
|
} else if (reason === 'ContainerCannotRun') {
|
|
101
163
|
problems.push({ severity: 'critical', message: 'Cannot Run' })
|
|
164
|
+
} else if (reason !== 'ContainerCreating' && reason !== 'PodInitializing') {
|
|
165
|
+
problems.push({ severity: 'high', message: reason })
|
|
102
166
|
}
|
|
103
167
|
}
|
|
104
168
|
// Check terminated state
|
|
105
169
|
if (cs.state?.terminated?.reason === 'OOMKilled') {
|
|
106
170
|
problems.push({ severity: 'critical', message: 'OOMKilled' })
|
|
171
|
+
} else if (cs.state?.terminated?.exitCode && cs.state.terminated.exitCode !== 0) {
|
|
172
|
+
problems.push({ severity: 'high', message: `Exit Code ${cs.state.terminated.exitCode}` })
|
|
107
173
|
}
|
|
108
174
|
// High restart count
|
|
109
175
|
if (cs.restartCount > 5) {
|
|
@@ -142,7 +208,7 @@ export function getPodProblems(pod: any): PodProblem[] {
|
|
|
142
208
|
}
|
|
143
209
|
|
|
144
210
|
// Evicted pods
|
|
145
|
-
if (
|
|
211
|
+
if (phase === 'Failed' && pod.status?.reason === 'Evicted') {
|
|
146
212
|
problems.push({ severity: 'high', message: 'Evicted' })
|
|
147
213
|
}
|
|
148
214
|
|
|
@@ -156,7 +222,6 @@ export function getPodProblems(pod: any): PodProblem[] {
|
|
|
156
222
|
}
|
|
157
223
|
|
|
158
224
|
// Not ready (Running but containers not ready)
|
|
159
|
-
const phase = pod.status?.phase
|
|
160
225
|
if (phase === 'Running') {
|
|
161
226
|
const readyContainers = containerStatuses.filter((c: any) => c.ready).length
|
|
162
227
|
const totalContainers = containerStatuses.length
|
|
@@ -335,6 +400,70 @@ export function getWorkloadStatus(resource: any, kind: string): StatusBadge {
|
|
|
335
400
|
return { text: `${ready}/${desired}`, color: healthColors.unhealthy, level: 'unhealthy' }
|
|
336
401
|
}
|
|
337
402
|
|
|
403
|
+
/** Detect problems for Deployments, StatefulSets, DaemonSets. Parallel to getPodProblems. */
|
|
404
|
+
export function getWorkloadProblems(resource: any, kind: string): PodProblem[] {
|
|
405
|
+
const problems: PodProblem[] = []
|
|
406
|
+
const status = resource.status || {}
|
|
407
|
+
const spec = resource.spec || {}
|
|
408
|
+
const k = kind.toLowerCase()
|
|
409
|
+
|
|
410
|
+
if (k === 'daemonsets') {
|
|
411
|
+
const desired = status.desiredNumberScheduled || 0
|
|
412
|
+
const ready = status.numberReady || 0
|
|
413
|
+
if (desired > 0 && ready === 0) {
|
|
414
|
+
problems.push({ severity: 'critical', message: 'No pods ready' })
|
|
415
|
+
} else if (desired > 0 && ready < desired) {
|
|
416
|
+
problems.push({ severity: 'high', message: `${desired - ready} pods unavailable` })
|
|
417
|
+
}
|
|
418
|
+
return problems
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Deployment, StatefulSet
|
|
422
|
+
const desired = spec.replicas ?? status.replicas ?? 0
|
|
423
|
+
const ready = status.readyReplicas || 0
|
|
424
|
+
const available = status.availableReplicas ?? ready
|
|
425
|
+
const updated = status.updatedReplicas || 0
|
|
426
|
+
|
|
427
|
+
if (desired === 0) return problems
|
|
428
|
+
|
|
429
|
+
if (ready === 0 && desired > 0) {
|
|
430
|
+
problems.push({ severity: 'critical', message: 'No pods ready' })
|
|
431
|
+
} else if (available < desired) {
|
|
432
|
+
problems.push({ severity: 'high', message: `${desired - available} pods unavailable` })
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (updated > 0 && updated < desired) {
|
|
436
|
+
problems.push({ severity: 'medium', message: 'Rollout in progress' })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check conditions for stuck rollouts (Deployment only)
|
|
440
|
+
if (k === 'deployments') {
|
|
441
|
+
const conditions = status.conditions || []
|
|
442
|
+
for (const cond of conditions) {
|
|
443
|
+
if (cond.type === 'Progressing' && cond.status === 'False' && cond.reason === 'ProgressDeadlineExceeded') {
|
|
444
|
+
problems.push({ severity: 'critical', message: 'Rollout stuck' })
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return problems
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Check whether a workload's problems match a given problem category */
|
|
453
|
+
export function workloadMatchesProblemCategory(problems: PodProblem[], category: string): boolean {
|
|
454
|
+
const msgs = problems.map(p => p.message)
|
|
455
|
+
switch (category) {
|
|
456
|
+
case 'Unavailable':
|
|
457
|
+
return msgs.some(m => m.includes('unavailable') || m.includes('No pods ready'))
|
|
458
|
+
case 'Rollout Stuck':
|
|
459
|
+
return msgs.includes('Rollout stuck')
|
|
460
|
+
case 'Rollout In Progress':
|
|
461
|
+
return msgs.includes('Rollout in progress')
|
|
462
|
+
default:
|
|
463
|
+
return false
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
338
467
|
export function getWorkloadImages(resource: any): string[] {
|
|
339
468
|
const containers = resource.spec?.template?.spec?.containers || []
|
|
340
469
|
return containers.map((c: any) => {
|
|
@@ -1325,37 +1454,6 @@ export function getRoleBindingSubjectCount(rb: any): number {
|
|
|
1325
1454
|
// WORKLOAD PROBLEM DETECTION (for table row indicators)
|
|
1326
1455
|
// ============================================================================
|
|
1327
1456
|
|
|
1328
|
-
export function getWorkloadProblems(resource: any, kind: string): string[] {
|
|
1329
|
-
const problems: string[] = []
|
|
1330
|
-
const status = resource.status || {}
|
|
1331
|
-
const spec = resource.spec || {}
|
|
1332
|
-
|
|
1333
|
-
if (kind === 'daemonsets') {
|
|
1334
|
-
const ready = status.numberReady || 0
|
|
1335
|
-
const desired = status.desiredNumberScheduled || 0
|
|
1336
|
-
if (desired > 0 && ready < desired) {
|
|
1337
|
-
problems.push(`${desired - ready} pods not ready`)
|
|
1338
|
-
}
|
|
1339
|
-
} else {
|
|
1340
|
-
const ready = status.readyReplicas || 0
|
|
1341
|
-
const desired = spec.replicas ?? 0
|
|
1342
|
-
if (desired > 0 && ready < desired) {
|
|
1343
|
-
problems.push(`${desired - ready} replicas not ready`)
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
const conditions = status.conditions || []
|
|
1348
|
-
for (const cond of conditions) {
|
|
1349
|
-
if (cond.status === 'True' && cond.type === 'ReplicaFailure') {
|
|
1350
|
-
problems.push('ReplicaFailure')
|
|
1351
|
-
}
|
|
1352
|
-
if (cond.status === 'False' && cond.type === 'Available') {
|
|
1353
|
-
problems.push('Unavailable')
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
return problems
|
|
1358
|
-
}
|
|
1359
1457
|
|
|
1360
1458
|
// ============================================================================
|
|
1361
1459
|
// FORMATTING UTILITIES
|