@skyhook-io/k8s-ui 1.13.3 → 1.14.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/package.json +4 -4
- package/src/components/applications/AppTooltips.tsx +13 -1
- package/src/components/applications/ApplicationDetail.test.tsx +25 -0
- package/src/components/applications/ApplicationDetail.tsx +39 -7
- package/src/components/applications/ApplicationsView.tsx +18 -3
- package/src/components/applications/ReadyBar.tsx +3 -2
- package/src/components/applications/applications-view.test.tsx +91 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -1
- package/src/components/issues/IssuesView.tsx +40 -31
- package/src/components/issues/ResourceIssuesSection.tsx +3 -2
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +123 -4
- package/src/components/issues/issues.test.ts +221 -10
- package/src/components/issues/types.ts +31 -5
- package/src/components/namespace-switcher/NamespacePicker.tsx +21 -13
- package/src/components/resources/ResourcesView.tsx +966 -98
- package/src/components/resources/column-settings-key.test.ts +98 -0
- package/src/components/resources/column-visibility-merge.test.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +236 -0
- package/src/components/resources/gateway-policy-status.test.ts +291 -0
- package/src/components/resources/gateway-policy-status.ts +237 -0
- package/src/components/resources/generic-status.test.ts +199 -0
- package/src/components/resources/generic-status.ts +171 -0
- package/src/components/resources/gpu-ecosystem-registry.test.ts +63 -0
- package/src/components/resources/index.ts +9 -1
- package/src/components/resources/istio-gateway-filter.test.ts +97 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +100 -1
- package/src/components/resources/kyverno-modern-posture.test.ts +36 -0
- package/src/components/resources/printer-columns.test.ts +298 -0
- package/src/components/resources/printer-columns.ts +249 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +29 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +3 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +69 -3
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +6 -2
- package/src/components/resources/renderers/EventRenderer.test.tsx +50 -1
- package/src/components/resources/renderers/EventRenderer.tsx +3 -5
- package/src/components/resources/renderers/GenericRenderer.tsx +18 -83
- package/src/components/resources/renderers/HPARenderer.test.tsx +28 -0
- package/src/components/resources/renderers/HPARenderer.tsx +6 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +12 -7
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +2 -1
- package/src/components/resources/renderers/PVCRenderer.test.tsx +44 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -2
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +92 -1
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +64 -8
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +22 -3
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +47 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +16 -1
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +44 -2
- package/src/components/resources/renderers/WorkloadRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +25 -65
- package/src/components/resources/renderers/amd-gpu-cells.tsx +36 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +12 -1
- package/src/components/resources/renderers/inference-gateway-cells.tsx +62 -0
- package/src/components/resources/renderers/jobset-lws-cells.tsx +82 -0
- package/src/components/resources/renderers/kai-cells.tsx +67 -0
- package/src/components/resources/renderers/kaito-cells.tsx +62 -0
- package/src/components/resources/renderers/kserve-cells.tsx +145 -0
- package/src/components/resources/renderers/kubeflow-training-cells.tsx +72 -0
- package/src/components/resources/renderers/kueue-cells.tsx +176 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +8 -4
- package/src/components/resources/renderers/nim-cells.tsx +81 -0
- package/src/components/resources/renderers/ray-cells.tsx +124 -0
- package/src/components/resources/renderers/volcano-cells.tsx +140 -0
- package/src/components/resources/resource-utils-amd-gpu.ts +61 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +13 -5
- package/src/components/resources/resource-utils-cnpg.test.ts +142 -2
- package/src/components/resources/resource-utils-cnpg.ts +132 -7
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +236 -0
- package/src/components/resources/resource-utils-hpa.test.ts +4 -0
- package/src/components/resources/resource-utils-hpa.ts +6 -2
- package/src/components/resources/resource-utils-inference-gateway.ts +119 -0
- package/src/components/resources/resource-utils-jobset-lws.ts +117 -0
- package/src/components/resources/resource-utils-kai.ts +105 -0
- package/src/components/resources/resource-utils-kaito.ts +114 -0
- package/src/components/resources/resource-utils-kserve.ts +186 -0
- package/src/components/resources/resource-utils-kubeflow-training.ts +121 -0
- package/src/components/resources/resource-utils-kueue.ts +200 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +3 -1
- package/src/components/resources/resource-utils-kyverno.ts +51 -2
- package/src/components/resources/resource-utils-nim.ts +109 -0
- package/src/components/resources/resource-utils-ray.ts +214 -0
- package/src/components/resources/resource-utils-rollout.test.ts +183 -0
- package/src/components/resources/resource-utils-velero.test.ts +30 -0
- package/src/components/resources/resource-utils-velero.ts +32 -0
- package/src/components/resources/resource-utils-volcano.ts +176 -0
- package/src/components/resources/resource-utils.ts +156 -47
- package/src/components/resources/resources-column-filter.test.ts +27 -1
- package/src/components/resources/workload-rollout-golden.test.ts +55 -0
- package/src/components/shared/CreateResourceDialog.tsx +1 -0
- package/src/components/shared/EditableYamlView.tsx +4 -2
- package/src/components/shared/ResourceActionsBar.tsx +170 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +206 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +84 -40
- package/src/components/shared/SetImageDialog.test.ts +129 -0
- package/src/components/shared/SetImageDialog.tsx +577 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/shared/revision-role-badges.test.ts +56 -4
- package/src/components/timeline/TimelineList.tsx +19 -3
- package/src/components/timeline/TimelineSwimlanes.tsx +11 -3
- package/src/components/trace/ReachabilityView.tsx +1 -1
- package/src/components/ui/SelectMenu.tsx +10 -1
- package/src/components/ui/drawer-components.tsx +12 -0
- package/src/components/workload/WorkloadRolloutNotice.tsx +60 -0
- package/src/components/workload/WorkloadView.tsx +144 -81
- package/src/hooks/useProgressiveRefresh.ts +37 -0
- package/src/source-hygiene.test.ts +28 -0
- package/src/theme/tailwind-theme.css +4 -0
- package/src/types/core.ts +55 -0
- package/src/utils/api-resources.test.ts +40 -1
- package/src/utils/api-resources.ts +11 -1
- package/src/utils/applications.ts +50 -0
- package/src/utils/bulk-workload-actions.test.ts +20 -0
- package/src/utils/bulk-workload-actions.ts +15 -0
- package/src/utils/navigation.test.ts +31 -1
- package/src/utils/navigation.ts +21 -2
- package/src/utils/workload-rollout.ts +292 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getClusterQueueCohort,
|
|
4
|
+
getKueueWorkloadPriority,
|
|
5
|
+
getKueueWorkloadStatus,
|
|
6
|
+
} from './resource-utils-kueue'
|
|
7
|
+
import { getRayClusterStatus, getRayJobStatus, getRayServiceStatus } from './resource-utils-ray'
|
|
8
|
+
import {
|
|
9
|
+
getInferenceServiceStatus,
|
|
10
|
+
getLLMInferenceServiceReplicas,
|
|
11
|
+
getServingRuntimeStatus,
|
|
12
|
+
} from './resource-utils-kserve'
|
|
13
|
+
import {
|
|
14
|
+
getInferenceObjectivePriority,
|
|
15
|
+
getInferenceObjectiveStatus,
|
|
16
|
+
getInferencePoolStatus,
|
|
17
|
+
} from './resource-utils-inference-gateway'
|
|
18
|
+
import { getJobSetStatus, getLeaderWorkerSetStatus } from './resource-utils-jobset-lws'
|
|
19
|
+
import { getVolcanoJobStatus, getVolcanoPodGroupStatus } from './resource-utils-volcano'
|
|
20
|
+
import { getKaiPodGroupStatus, getKaiQueuePriority, getKaiQueueQuota, getKaiQueueStatus } from './resource-utils-kai'
|
|
21
|
+
import { getKaitoWorkspaceStatus, getRAGEngineStatus } from './resource-utils-kaito'
|
|
22
|
+
import { getNIMCacheStatus, getNIMServiceReplicas } from './resource-utils-nim'
|
|
23
|
+
import { getAMDDeviceConfigStatus } from './resource-utils-amd-gpu'
|
|
24
|
+
import { getTrainingJobReplicas, getTrainJobStatus, trainingJobStatus } from './resource-utils-kubeflow-training'
|
|
25
|
+
|
|
26
|
+
describe('GPU ecosystem API contracts', () => {
|
|
27
|
+
it('reads current Kueue v1beta2 fields and condition precedence', () => {
|
|
28
|
+
const workload = {
|
|
29
|
+
spec: { priorityClassRef: { name: 'critical' } },
|
|
30
|
+
status: { conditions: [
|
|
31
|
+
{ type: 'Admitted', status: 'True' },
|
|
32
|
+
{ type: 'Evicted', status: 'True' },
|
|
33
|
+
] },
|
|
34
|
+
}
|
|
35
|
+
expect(getClusterQueueCohort({ spec: { cohortName: 'gpu' } })).toBe('gpu')
|
|
36
|
+
expect(getKueueWorkloadPriority(workload)).toBe('critical')
|
|
37
|
+
expect(getKueueWorkloadStatus(workload).text).toBe('Evicted')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('distinguishes Kueue terminal failures from successful workloads', () => {
|
|
41
|
+
for (const reason of ['Failed', 'FailedToStart', 'OutOfSync', 'OwnerNotFound']) {
|
|
42
|
+
expect(getKueueWorkloadStatus({
|
|
43
|
+
status: { conditions: [{ type: 'Finished', status: 'True', reason }] },
|
|
44
|
+
})).toMatchObject({ text: reason, level: 'unhealthy' })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const reason of ['Succeeded', 'WorkloadSliceReplaced']) {
|
|
48
|
+
expect(getKueueWorkloadStatus({
|
|
49
|
+
status: { conditions: [{ type: 'Finished', status: 'True', reason }] },
|
|
50
|
+
})).toMatchObject({ text: 'Finished', level: 'neutral' })
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('lets KubeRay conditions outrank deprecated state fields', () => {
|
|
55
|
+
expect(getRayClusterStatus({
|
|
56
|
+
status: {
|
|
57
|
+
state: 'ready',
|
|
58
|
+
conditions: [{ type: 'HeadPodReady', status: 'False', reason: 'HeadUnavailable' }],
|
|
59
|
+
},
|
|
60
|
+
}).text).toBe('HeadUnavailable')
|
|
61
|
+
expect(getRayServiceStatus({
|
|
62
|
+
status: {
|
|
63
|
+
serviceStatus: 'Running',
|
|
64
|
+
conditions: [{ type: 'Ready', status: 'False', reason: 'ServeNotReady' }],
|
|
65
|
+
},
|
|
66
|
+
}).text).toBe('ServeNotReady')
|
|
67
|
+
expect(getRayServiceStatus({
|
|
68
|
+
spec: { suspend: true },
|
|
69
|
+
status: { conditions: [{ type: 'Suspending', status: 'True' }] },
|
|
70
|
+
}).text).toBe('Suspending')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('requires both the KubeRay head and workers before reporting Ready', () => {
|
|
74
|
+
expect(getRayClusterStatus({
|
|
75
|
+
status: {
|
|
76
|
+
conditions: [{ type: 'HeadPodReady', status: 'True' }],
|
|
77
|
+
availableWorkerReplicas: 1,
|
|
78
|
+
desiredWorkerReplicas: 2,
|
|
79
|
+
},
|
|
80
|
+
}).text).toBe('Workers 1/2')
|
|
81
|
+
expect(getRayClusterStatus({
|
|
82
|
+
status: {
|
|
83
|
+
conditions: [{ type: 'HeadPodReady', status: 'True' }],
|
|
84
|
+
availableWorkerReplicas: 2,
|
|
85
|
+
desiredWorkerReplicas: 2,
|
|
86
|
+
},
|
|
87
|
+
}).text).toBe('Ready')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('surfaces current KubeRay replica and RayJob deployment failures', () => {
|
|
91
|
+
expect(getRayClusterStatus({
|
|
92
|
+
status: {
|
|
93
|
+
conditions: [{ type: 'RayClusterReplicaFailure', status: 'True', reason: 'FailedCreateWorkerPod' }],
|
|
94
|
+
},
|
|
95
|
+
}).text).toBe('FailedCreateWorkerPod')
|
|
96
|
+
expect(getRayJobStatus({
|
|
97
|
+
status: { jobStatus: 'PENDING', jobDeploymentStatus: 'ValidationFailed' },
|
|
98
|
+
}).text).toBe('ValidationFailed')
|
|
99
|
+
expect(getRayJobStatus({
|
|
100
|
+
status: { jobStatus: 'RUNNING', jobDeploymentStatus: 'Failed' },
|
|
101
|
+
}).text).toBe('Failed')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('does not claim KServe runtime health from enabled configuration alone', () => {
|
|
105
|
+
const enabled = getServingRuntimeStatus({ spec: {} })
|
|
106
|
+
expect(enabled.text).toBe('Enabled')
|
|
107
|
+
expect(enabled.level).toBe('neutral')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('presents KServe transition failures as operator-readable status', () => {
|
|
111
|
+
expect(getInferenceServiceStatus({
|
|
112
|
+
status: { modelStatus: { transitionStatus: 'BlockedByFailedLoad' } },
|
|
113
|
+
}).text).toBe('Load failed')
|
|
114
|
+
expect(getInferenceServiceStatus({
|
|
115
|
+
status: { modelStatus: { transitionStatus: 'InvalidSpec' } },
|
|
116
|
+
}).text).toBe('Invalid spec')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('reads the inline replicas field in both served LLMInferenceService versions', () => {
|
|
120
|
+
expect(getLLMInferenceServiceReplicas({ spec: { replicas: 3 } })).toBe('3')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('understands current llm-d and deployed InferenceObjective conditions', () => {
|
|
124
|
+
expect(getInferenceObjectiveStatus({
|
|
125
|
+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
|
|
126
|
+
}).text).toBe('Accepted')
|
|
127
|
+
expect(getInferenceObjectiveStatus({
|
|
128
|
+
status: { conditions: [{ type: 'Accepted', status: 'False', reason: 'InvalidPool' }] },
|
|
129
|
+
}).text).toBe('InvalidPool')
|
|
130
|
+
expect(getInferenceObjectiveStatus({ status: { conditions: [] } }).text).toBe('Unknown')
|
|
131
|
+
expect(getInferenceObjectivePriority({ spec: {} })).toBe('0')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('does not treat the alpha InferencePool sentinel parent as a Gateway', () => {
|
|
135
|
+
expect(getInferencePoolStatus({
|
|
136
|
+
status: { parent: [{ parentRef: { kind: 'Status', name: 'default' } }] },
|
|
137
|
+
}).text).toBe('Not referenced')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('distinguishes pending JobSets from active work', () => {
|
|
141
|
+
expect(getJobSetStatus({ status: { replicatedJobsStatus: [{ active: 0, ready: 0 }] } }).text).toBe('Pending')
|
|
142
|
+
expect(getJobSetStatus({ status: { replicatedJobsStatus: [{ active: 1, ready: 0 }] } }).text).toBe('Running')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('surfaces an unavailable LeaderWorkerSet before progress', () => {
|
|
146
|
+
expect(getLeaderWorkerSetStatus({
|
|
147
|
+
status: { conditions: [{ type: 'Available', status: 'False', reason: 'PodsNotReady' }] },
|
|
148
|
+
}).text).toBe('PodsNotReady')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('classifies Volcano terminal and unschedulable states', () => {
|
|
152
|
+
expect(getVolcanoJobStatus({ status: { state: { phase: 'Failed' } } }).level).toBe('unhealthy')
|
|
153
|
+
expect(getVolcanoPodGroupStatus({
|
|
154
|
+
status: { conditions: [{ type: 'Unschedulable', status: 'True' }] },
|
|
155
|
+
}).text).toBe('Unschedulable')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('reads KAI quota units and scheduling failures', () => {
|
|
159
|
+
expect(getKaiQueueQuota({
|
|
160
|
+
spec: { resources: { gpu: { quota: 2 }, cpu: { quota: 500 }, memory: { quota: -1 } } },
|
|
161
|
+
})).toBe('GPU: 2, CPU: 500m, Mem: unlimited')
|
|
162
|
+
expect(getKaiQueuePriority({ spec: {} })).toBe('100')
|
|
163
|
+
expect(getKaiQueueStatus({
|
|
164
|
+
status: { conditions: [{ type: 'Orphan', status: 'True', reason: 'ParentNotFound' }] },
|
|
165
|
+
}).text).toBe('ParentNotFound')
|
|
166
|
+
expect(getKaiPodGroupStatus({
|
|
167
|
+
status: { schedulingConditions: [{ type: 'UnschedulableOnNodePool', status: 'True' }] },
|
|
168
|
+
}).text).toBe('Unschedulable')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('uses KAITO v1beta1 state and RAGEngine conditions', () => {
|
|
172
|
+
expect(getKaitoWorkspaceStatus({
|
|
173
|
+
status: { state: 'Failed', conditions: [{ type: 'ResourceReady', status: 'True' }] },
|
|
174
|
+
}).level).toBe('unhealthy')
|
|
175
|
+
expect(getKaitoWorkspaceStatus({ status: { state: 'Pending' } }).text).toBe('Pending')
|
|
176
|
+
expect(getRAGEngineStatus({
|
|
177
|
+
status: { conditions: [{ type: 'ServiceReady', status: 'False', reason: 'DeploymentUnavailable' }] },
|
|
178
|
+
}).text).toBe('DeploymentUnavailable')
|
|
179
|
+
expect(getRAGEngineStatus({
|
|
180
|
+
status: { conditions: [
|
|
181
|
+
{ type: 'ResourceReady', status: 'True' },
|
|
182
|
+
{ type: 'ServiceReady', status: 'True' },
|
|
183
|
+
] },
|
|
184
|
+
}).text).toBe('Ready')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('reads NIM state and scaling bounds', () => {
|
|
188
|
+
expect(getNIMCacheStatus({ status: { state: 'Failed' } }).level).toBe('unhealthy')
|
|
189
|
+
expect(getNIMServiceReplicas({
|
|
190
|
+
spec: { scale: { enabled: true, hpa: { minReplicas: 1, maxReplicas: 4 } } },
|
|
191
|
+
status: { availableReplicas: 2 },
|
|
192
|
+
})).toBe('2 (1-4)')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('lets AMD DeviceConfig readiness conditions outrank rollout counts', () => {
|
|
196
|
+
expect(getAMDDeviceConfigStatus({
|
|
197
|
+
status: {
|
|
198
|
+
conditions: [{ type: 'Ready', status: 'False', reason: 'DriverInstallFailed' }],
|
|
199
|
+
driver: { desiredNumber: 2, availableNumber: 2 },
|
|
200
|
+
devicePlugin: { desiredNumber: 2, availableNumber: 2 },
|
|
201
|
+
},
|
|
202
|
+
}).text).toBe('DriverInstallFailed')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('uses semantic precedence for Kubeflow conditions instead of array order', () => {
|
|
206
|
+
expect(trainingJobStatus({
|
|
207
|
+
status: { conditions: [
|
|
208
|
+
{ type: 'Failed', status: 'True' },
|
|
209
|
+
{ type: 'Running', status: 'True' },
|
|
210
|
+
] },
|
|
211
|
+
}).text).toBe('Failed')
|
|
212
|
+
expect(getTrainJobStatus({
|
|
213
|
+
spec: { suspend: true },
|
|
214
|
+
status: { conditions: [] },
|
|
215
|
+
}).text).toBe('Suspended')
|
|
216
|
+
expect(getTrainJobStatus({
|
|
217
|
+
status: { conditions: [{ type: 'Suspended', status: 'False' }] },
|
|
218
|
+
}).text).toBe('Pending')
|
|
219
|
+
expect(getTrainJobStatus({
|
|
220
|
+
status: { jobsStatus: [{ name: 'trainer', active: 1 }] },
|
|
221
|
+
}).text).toBe('Running')
|
|
222
|
+
expect(getTrainJobStatus({
|
|
223
|
+
status: { conditions: [{ type: 'Failed', status: 'True' }] },
|
|
224
|
+
}).text).toBe('Failed')
|
|
225
|
+
expect(getTrainJobStatus({
|
|
226
|
+
status: {
|
|
227
|
+
conditions: [{ type: 'Complete', status: 'True' }],
|
|
228
|
+
jobsStatus: [{ name: 'trainer', active: 1 }],
|
|
229
|
+
},
|
|
230
|
+
}).text).toBe('Complete')
|
|
231
|
+
expect(getTrainingJobReplicas({
|
|
232
|
+
spec: { pytorchReplicaSpecs: { Worker: { replicas: 4 } } },
|
|
233
|
+
status: { replicaStatuses: { Worker: { active: 1, succeeded: 1, failed: 2 } } },
|
|
234
|
+
})).toBe('Worker 2/4 (2 failed)')
|
|
235
|
+
})
|
|
236
|
+
})
|
|
@@ -22,6 +22,10 @@ describe('hpaStateLabel', () => {
|
|
|
22
22
|
it('keeps max-bound wording terse for table cells', () => {
|
|
23
23
|
expect(hpaStateLabel('limited_max')).toBe('Maxed')
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('describes an intentional zero-replica target', () => {
|
|
27
|
+
expect(hpaStateLabel('scaled_to_zero')).toBe('Scaled to zero')
|
|
28
|
+
})
|
|
25
29
|
})
|
|
26
30
|
|
|
27
31
|
function loadCases(): FixtureCase[] {
|
|
@@ -12,9 +12,10 @@ export function getHPATableState(hpa: any): HPADiagnosisState {
|
|
|
12
12
|
if (conditionStatus(conditions, 'AbleToScale') === 'False') return 'unable_to_scale'
|
|
13
13
|
const scalingActive = condition(conditions, 'ScalingActive')
|
|
14
14
|
if (scalingActive?.status === 'False') {
|
|
15
|
-
if (isScalingDisabled(scalingActive)) return '
|
|
16
|
-
return 'metrics_unavailable'
|
|
15
|
+
if (!isScalingDisabled(scalingActive)) return 'metrics_unavailable'
|
|
17
16
|
}
|
|
17
|
+
if (conditionStatus(conditions, 'ScaledToZero') === 'True' && min === 0 && desired === 0) return 'scaled_to_zero'
|
|
18
|
+
if (scalingActive?.status === 'False' && isScalingDisabled(scalingActive)) return 'disabled'
|
|
18
19
|
if (max > 0 && min === max && current === desired && desired === max) return 'pinned'
|
|
19
20
|
if (isMaxLimited(condition(conditions, 'ScalingLimited'))) return 'limited_max'
|
|
20
21
|
|
|
@@ -31,6 +32,8 @@ export function hpaStateLabel(state: HPADiagnosisState): string {
|
|
|
31
32
|
return 'Metrics unavailable'
|
|
32
33
|
case 'metrics_incomplete':
|
|
33
34
|
return 'Metrics incomplete'
|
|
35
|
+
case 'scaled_to_zero':
|
|
36
|
+
return 'Scaled to zero'
|
|
34
37
|
case 'disabled':
|
|
35
38
|
return 'Disabled'
|
|
36
39
|
case 'pinned':
|
|
@@ -66,6 +69,7 @@ export function hpaStateLevel(state: HPADiagnosisState): HealthLevel {
|
|
|
66
69
|
case 'scaling_down':
|
|
67
70
|
return 'degraded'
|
|
68
71
|
case 'limited_min':
|
|
72
|
+
case 'scaled_to_zero':
|
|
69
73
|
case 'disabled':
|
|
70
74
|
case 'pinned':
|
|
71
75
|
case 'stabilized':
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Gateway API Inference Extension CRD utility functions.
|
|
2
|
+
// InferencePool exists in two shapes: v1 (inference.networking.k8s.io) and
|
|
3
|
+
// v1alpha2 (inference.networking.x-k8s.io); accessors handle both.
|
|
4
|
+
|
|
5
|
+
import type { StatusBadge } from './resource-utils'
|
|
6
|
+
import { healthColors } from './resource-utils'
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// INFERENCEPOOL UTILITIES
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
// v1 serializes status.parents, v1alpha2 serializes status.parent
|
|
13
|
+
function getPoolParents(resource: any): any[] {
|
|
14
|
+
const status = resource.status
|
|
15
|
+
if (!status) return []
|
|
16
|
+
if (Array.isArray(status.parents)) return status.parents
|
|
17
|
+
if (Array.isArray(status.parent)) return status.parent
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// v1alpha2 reserves a synthetic parent entry to mean "no gateway references
|
|
22
|
+
// this pool" — seen as parentRef {kind: Status, name: default} or as an
|
|
23
|
+
// empty/missing parentRef. A ref without a name can't be a real Gateway.
|
|
24
|
+
function isDefaultParent(parent: any): boolean {
|
|
25
|
+
const ref = parent?.parentRef
|
|
26
|
+
if (!ref?.name) return true
|
|
27
|
+
return ref.kind === 'Status' && ref.name === 'default'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getInferencePoolStatus(resource: any): StatusBadge {
|
|
31
|
+
const parents = getPoolParents(resource).filter((p: any) => !isDefaultParent(p))
|
|
32
|
+
if (parents.length === 0) {
|
|
33
|
+
return { text: 'Not referenced', color: healthColors.neutral, level: 'neutral' }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const conditions = parents.flatMap((p: any) => (Array.isArray(p?.conditions) ? p.conditions : []))
|
|
37
|
+
if (conditions.length === 0) {
|
|
38
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvedRefsFailed = conditions.find((c: any) => c?.type === 'ResolvedRefs' && c.status === 'False')
|
|
42
|
+
if (resolvedRefsFailed) {
|
|
43
|
+
return { text: resolvedRefsFailed.reason || 'RefsNotResolved', color: healthColors.alert, level: 'alert' }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const accepted = conditions.filter((c: any) => c?.type === 'Accepted')
|
|
47
|
+
const acceptedTrue = accepted.filter((c: any) => c.status === 'True')
|
|
48
|
+
if (accepted.length > 0 && acceptedTrue.length === accepted.length) {
|
|
49
|
+
return { text: 'Accepted', color: healthColors.healthy, level: 'healthy' }
|
|
50
|
+
}
|
|
51
|
+
if (acceptedTrue.length > 0) {
|
|
52
|
+
return { text: 'Partially accepted', color: healthColors.degraded, level: 'degraded' }
|
|
53
|
+
}
|
|
54
|
+
const notAccepted = accepted.find((c: any) => c.status === 'False')
|
|
55
|
+
if (notAccepted) {
|
|
56
|
+
return { text: notAccepted.reason || 'NotAccepted', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getInferencePoolSelector(resource: any): string {
|
|
63
|
+
const selector = resource.spec?.selector
|
|
64
|
+
if (!selector || typeof selector !== 'object') return '-'
|
|
65
|
+
const labels = selector.matchLabels && typeof selector.matchLabels === 'object' ? selector.matchLabels : selector
|
|
66
|
+
const entries = Object.entries(labels).filter(([, v]) => typeof v === 'string' || typeof v === 'number')
|
|
67
|
+
if (entries.length === 0) return '-'
|
|
68
|
+
const parts = entries.map(([k, v]) => `${k}=${v}`)
|
|
69
|
+
if (parts.length > 2) return `${parts.slice(0, 2).join(', ')} +${parts.length - 2}`
|
|
70
|
+
return parts.join(', ')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getInferencePoolTargetPorts(resource: any): string {
|
|
74
|
+
const spec = resource.spec
|
|
75
|
+
if (!spec) return '-'
|
|
76
|
+
if (Array.isArray(spec.targetPorts)) {
|
|
77
|
+
const numbers = spec.targetPorts
|
|
78
|
+
.map((p: any) => p?.number)
|
|
79
|
+
.filter((n: any) => n !== undefined && n !== null)
|
|
80
|
+
return numbers.length > 0 ? numbers.join(', ') : '-'
|
|
81
|
+
}
|
|
82
|
+
if (spec.targetPortNumber !== undefined && spec.targetPortNumber !== null) {
|
|
83
|
+
return String(spec.targetPortNumber)
|
|
84
|
+
}
|
|
85
|
+
return '-'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getInferencePoolExtensionRef(resource: any): string {
|
|
89
|
+
const ref = resource.spec?.endpointPickerRef || resource.spec?.extensionRef
|
|
90
|
+
return ref?.name || '-'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// INFERENCEOBJECTIVE UTILITIES
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
export function getInferenceObjectiveStatus(resource: any): StatusBadge {
|
|
98
|
+
const conditions = Array.isArray(resource.status?.conditions) ? resource.status.conditions : []
|
|
99
|
+
const accepted = conditions.find((c: any) => c?.type === 'Accepted')
|
|
100
|
+
|| conditions.find((c: any) => c?.type === 'Ready')
|
|
101
|
+
if (accepted?.status === 'True') {
|
|
102
|
+
return { text: 'Accepted', color: healthColors.healthy, level: 'healthy' }
|
|
103
|
+
}
|
|
104
|
+
if (accepted?.status === 'False') {
|
|
105
|
+
return { text: accepted.reason || 'NotAccepted', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
106
|
+
}
|
|
107
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getInferenceObjectivePoolRef(resource: any): string {
|
|
111
|
+
return resource.spec?.poolRef?.name || '-'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// API spec: consumers treat an unset priority as 0
|
|
115
|
+
export function getInferenceObjectivePriority(resource: any): string {
|
|
116
|
+
const priority = resource.spec?.priority
|
|
117
|
+
if (priority === undefined || priority === null) return '0'
|
|
118
|
+
return String(priority)
|
|
119
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// LeaderWorkerSet (leaderworkerset.x-k8s.io/v1) and JobSet (jobset.x-k8s.io/v1alpha2) utility functions
|
|
2
|
+
|
|
3
|
+
import type { StatusBadge } from './resource-utils'
|
|
4
|
+
import { healthColors } from './resource-utils'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// LEADERWORKERSET UTILITIES
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export function getLeaderWorkerSetStatus(resource: any): StatusBadge {
|
|
11
|
+
const conditions = resource.status?.conditions || []
|
|
12
|
+
|
|
13
|
+
const availableCond = conditions.find((c: any) => c.type === 'Available')
|
|
14
|
+
if (availableCond?.status === 'True') {
|
|
15
|
+
return { text: 'Available', color: healthColors.healthy, level: 'healthy' }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const updateCond = conditions.find((c: any) => c.type === 'UpdateInProgress')
|
|
19
|
+
if (updateCond?.status === 'True') {
|
|
20
|
+
return { text: 'Updating', color: healthColors.degraded, level: 'degraded' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const progressingCond = conditions.find((c: any) => c.type === 'Progressing')
|
|
24
|
+
if (progressingCond?.status === 'True') {
|
|
25
|
+
return { text: 'Progressing', color: healthColors.degraded, level: 'degraded' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (availableCond?.status === 'False') {
|
|
29
|
+
return { text: availableCond.reason || 'Unavailable', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getLeaderWorkerSetReplicas(resource: any): string {
|
|
36
|
+
const replicas = resource.spec?.replicas
|
|
37
|
+
return replicas !== undefined && replicas !== null ? String(replicas) : '1'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getLeaderWorkerSetSize(resource: any): string {
|
|
41
|
+
const size = resource.spec?.leaderWorkerTemplate?.size
|
|
42
|
+
return size !== undefined && size !== null ? String(size) : '1'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getLeaderWorkerSetReady(resource: any): string {
|
|
46
|
+
const ready = resource.status?.readyReplicas ?? 0
|
|
47
|
+
const desired = resource.spec?.replicas ?? 1
|
|
48
|
+
return `${ready}/${desired}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getLeaderWorkerSetUpdated(resource: any): string {
|
|
52
|
+
const updated = resource.status?.updatedReplicas
|
|
53
|
+
return updated !== undefined && updated !== null ? String(updated) : '0'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// JOBSET UTILITIES
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
export function getJobSetStatus(resource: any): StatusBadge {
|
|
61
|
+
const conditions = resource.status?.conditions || []
|
|
62
|
+
|
|
63
|
+
const failedCond = conditions.find((c: any) => c.type === 'Failed')
|
|
64
|
+
if (failedCond?.status === 'True') {
|
|
65
|
+
return { text: failedCond.reason || 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const completedCond = conditions.find((c: any) => c.type === 'Completed')
|
|
69
|
+
if (completedCond?.status === 'True') {
|
|
70
|
+
return { text: 'Completed', color: healthColors.neutral, level: 'neutral' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const suspendedCond = conditions.find((c: any) => c.type === 'Suspended')
|
|
74
|
+
if (suspendedCond?.status === 'True' || resource.spec?.suspend === true) {
|
|
75
|
+
return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Running only when child jobs are actually live — a fresh JobSet carries a
|
|
79
|
+
// minimal status (no conditions, zeroed counts) before reconciliation.
|
|
80
|
+
const live = sumReplicatedJobsField(resource, 'active') + sumReplicatedJobsField(resource, 'ready')
|
|
81
|
+
if (live > 0) {
|
|
82
|
+
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
83
|
+
}
|
|
84
|
+
if (resource.status) {
|
|
85
|
+
return { text: 'Pending', color: healthColors.neutral, level: 'neutral' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getJobSetReplicatedJobs(resource: any): string {
|
|
92
|
+
const jobs = resource.spec?.replicatedJobs
|
|
93
|
+
if (!Array.isArray(jobs) || jobs.length === 0) return '-'
|
|
94
|
+
return String(jobs.length)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sumReplicatedJobsField(resource: any, field: string): number {
|
|
98
|
+
const statuses = resource.status?.replicatedJobsStatus
|
|
99
|
+
if (!Array.isArray(statuses)) return 0
|
|
100
|
+
return statuses.reduce((sum: number, s: any) => sum + (s?.[field] ?? 0), 0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getJobSetReadyJobs(resource: any): number {
|
|
104
|
+
return sumReplicatedJobsField(resource, 'ready')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getJobSetSucceededJobs(resource: any): number {
|
|
108
|
+
return sumReplicatedJobsField(resource, 'succeeded')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getJobSetFailedJobs(resource: any): number {
|
|
112
|
+
return sumReplicatedJobsField(resource, 'failed')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getJobSetRestarts(resource: any): number {
|
|
116
|
+
return resource.status?.restarts ?? 0
|
|
117
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// KAI Scheduler CRD utility functions
|
|
2
|
+
|
|
3
|
+
import type { StatusBadge } from './resource-utils'
|
|
4
|
+
import { healthColors } from './resource-utils'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// SHARED HELPERS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
function summarizeResourceList(list: any): string {
|
|
11
|
+
if (!list || typeof list !== 'object') return '-'
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
if (list.cpu !== undefined) parts.push(`CPU: ${list.cpu}`)
|
|
14
|
+
if (list.memory !== undefined) parts.push(`Mem: ${list.memory}`)
|
|
15
|
+
if (list['nvidia.com/gpu'] !== undefined) parts.push(`GPU: ${list['nvidia.com/gpu']}`)
|
|
16
|
+
const rest = Object.keys(list).filter((k) => k !== 'cpu' && k !== 'memory' && k !== 'nvidia.com/gpu')
|
|
17
|
+
if (rest.length > 0) parts.push(`+${rest.length} more`)
|
|
18
|
+
return parts.length > 0 ? parts.join(', ') : '-'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// KAI quota units: GPU in fractions, CPU in millicpus, memory in megabytes; -1 means unlimited
|
|
22
|
+
function formatKaiQuota(value: any, unit: string): string {
|
|
23
|
+
if (typeof value !== 'number') return '-'
|
|
24
|
+
if (value === -1) return 'unlimited'
|
|
25
|
+
return `${value}${unit}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// KAI QUEUE UTILITIES (scheduling.run.ai/v2, cluster-scoped)
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export function getKaiQueueStatus(resource: any): StatusBadge {
|
|
33
|
+
const conditions = resource.status?.conditions || []
|
|
34
|
+
const orphan = conditions.find((condition: any) => condition?.type === 'Orphan' && condition?.status === 'True')
|
|
35
|
+
if (orphan) {
|
|
36
|
+
return { text: orphan.reason || 'Orphan', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
37
|
+
}
|
|
38
|
+
const overQuota = conditions.find((condition: any) => condition?.type === 'OverQuota' && condition?.status === 'True')
|
|
39
|
+
if (overQuota) {
|
|
40
|
+
return { text: overQuota.reason || 'Over quota', color: healthColors.degraded, level: 'degraded' }
|
|
41
|
+
}
|
|
42
|
+
return { text: 'Configured', color: healthColors.neutral, level: 'neutral' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getKaiQueueParent(resource: any): string {
|
|
46
|
+
return resource.spec?.parentQueue || '-'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getKaiQueuePriority(resource: any): string {
|
|
50
|
+
const priority = resource.spec?.priority
|
|
51
|
+
return priority !== undefined && priority !== null ? String(priority) : '100'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getKaiQueueQuota(resource: any): string {
|
|
55
|
+
const resources = resource.spec?.resources
|
|
56
|
+
if (!resources) return '-'
|
|
57
|
+
const parts: string[] = []
|
|
58
|
+
if (typeof resources.gpu?.quota === 'number') parts.push(`GPU: ${formatKaiQuota(resources.gpu.quota, '')}`)
|
|
59
|
+
if (typeof resources.cpu?.quota === 'number') parts.push(`CPU: ${formatKaiQuota(resources.cpu.quota, 'm')}`)
|
|
60
|
+
if (typeof resources.memory?.quota === 'number') parts.push(`Mem: ${formatKaiQuota(resources.memory.quota, 'MB')}`)
|
|
61
|
+
return parts.length > 0 ? parts.join(', ') : '-'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getKaiQueueAllocated(resource: any): string {
|
|
65
|
+
return summarizeResourceList(resource.status?.allocated)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// KAI PODGROUP UTILITIES (scheduling.run.ai/v2alpha2)
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export function getKaiPodGroupStatus(resource: any): StatusBadge {
|
|
73
|
+
const phase = resource.status?.phase
|
|
74
|
+
if (phase === 'Running') {
|
|
75
|
+
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
76
|
+
}
|
|
77
|
+
const conditions = resource.status?.conditions || []
|
|
78
|
+
const schedulingConditions = resource.status?.schedulingConditions || []
|
|
79
|
+
const unschedulable =
|
|
80
|
+
conditions.some((c: any) => c.type === 'Unschedulable' && c.status === 'True') ||
|
|
81
|
+
schedulingConditions.some((c: any) => c.type === 'UnschedulableOnNodePool' && c.status === 'True')
|
|
82
|
+
if (unschedulable) {
|
|
83
|
+
return { text: 'Unschedulable', color: healthColors.alert, level: 'alert' }
|
|
84
|
+
}
|
|
85
|
+
if (phase === 'Pending' || phase === 'Inqueue' || phase === 'Completed' || phase === 'Succeeded') {
|
|
86
|
+
return { text: phase, color: healthColors.neutral, level: 'neutral' }
|
|
87
|
+
}
|
|
88
|
+
if (phase === 'Failed') {
|
|
89
|
+
return { text: 'Failed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
90
|
+
}
|
|
91
|
+
// OSS KAI components never write status.phase (Run:ai platform does); fall back to pod counts
|
|
92
|
+
if (!phase && (resource.status?.running || 0) > 0) {
|
|
93
|
+
return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
|
|
94
|
+
}
|
|
95
|
+
return { text: phase || 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getKaiPodGroupQueue(resource: any): string {
|
|
99
|
+
return resource.spec?.queue || '-'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getKaiPodGroupMinMember(resource: any): string {
|
|
103
|
+
const min = resource.spec?.minMember
|
|
104
|
+
return min !== undefined && min !== null ? String(min) : '-'
|
|
105
|
+
}
|