@skyhook-io/k8s-ui 1.14.9 → 1.14.11
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 +3 -2
- package/src/components/applications/ApplicationDetail.tsx +24 -11
- package/src/components/audit/AuditAlerts.tsx +1 -3
- package/src/components/audit/AuditCoveragePresentation.test.tsx +48 -2
- package/src/components/checks/ChecksView.test.tsx +55 -0
- package/src/components/checks/ChecksView.tsx +38 -27
- package/src/components/dock/NodeTerminalTab.test.tsx +89 -0
- package/src/components/dock/NodeTerminalTab.tsx +51 -47
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +17 -9
- package/src/components/gitops/detail-helpers.test.ts +32 -0
- package/src/components/gitops/detail-helpers.ts +21 -0
- package/src/components/gitops/health-provenance.test.ts +4 -3
- package/src/components/gitops/index.ts +2 -1
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +131 -54
- package/src/components/issues/IssuesView.tsx +6 -0
- package/src/components/issues/ResourceIssuesSection.test.tsx +36 -0
- package/src/components/issues/diagnostic.ts +4 -0
- package/src/components/logs/LogCore.tsx +3 -3
- package/src/components/logs/WorkloadLogsViewer.tsx +41 -11
- package/src/components/logs/useLogBuffer.ts +1 -0
- package/src/components/logs/useLogStream.ts +1 -2
- package/src/components/resources/KueueAdmissionSection.test.tsx +95 -0
- package/src/components/resources/KueueAdmissionSection.tsx +87 -0
- package/src/components/resources/ResourcesView.default-sort.test.ts +26 -0
- package/src/components/resources/ResourcesView.tsx +91 -16
- package/src/components/resources/curated-column-ownership.test.ts +45 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +4 -3
- package/src/components/resources/renderers/ContainerEnvironmentSection.tsx +10 -3
- package/src/components/resources/renderers/JobSetRenderer.test.tsx +280 -0
- package/src/components/resources/renderers/JobSetRenderer.tsx +322 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.test.tsx +67 -0
- package/src/components/resources/renderers/KueueProvisioningRenderers.tsx +91 -0
- package/src/components/resources/renderers/KueueQueueNavigation.test.tsx +69 -0
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +123 -0
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +299 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +121 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +230 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +25 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +93 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +62 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +114 -0
- package/src/components/resources/renderers/index.ts +11 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +103 -2
- package/src/components/resources/resource-utils-jobset-lws.ts +43 -9
- package/src/components/resources/resource-utils-kueue.ts +145 -50
- package/src/components/resources/resource-utils-ray.test.ts +58 -1
- package/src/components/resources/resource-utils-ray.ts +56 -90
- package/src/components/shared/ResourceRendererDispatch.test.tsx +117 -2
- package/src/components/shared/ResourceRendererDispatch.tsx +47 -10
- package/src/components/timeline/TimelineList.tsx +5 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +15 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +43 -23
- package/src/components/topology/K8sResourceNode.tsx +3 -3
- package/src/components/ui/CodeViewer.tsx +4 -0
- package/src/components/ui/Toast.tsx +9 -3
- package/src/components/ui/drawer-components.tsx +3 -1
- package/src/components/workload/PodList.tsx +154 -0
- package/src/components/workload/WorkloadView.tsx +29 -159
- package/src/components/workload/index.ts +11 -1
- package/src/components/workload/workload-logs-availability.test.ts +20 -0
- package/src/components/workload/workload-pod-presentation.test.ts +27 -0
- package/src/types/core.ts +24 -0
- package/src/types/gitops-tree.ts +5 -2
- package/src/types/scheduling.ts +71 -0
- package/src/utils/api-resources.test.ts +20 -1
- package/src/utils/api-resources.ts +26 -0
- package/src/utils/application-topology.test.ts +25 -0
- package/src/utils/application-topology.ts +2 -1
- package/src/utils/applications.test.ts +82 -0
- package/src/utils/applications.ts +62 -24
- package/src/utils/gitops-route.test.ts +20 -1
- package/src/utils/gitops-route.ts +15 -7
- package/src/utils/log-format.ts +5 -2
- package/src/utils/log-stream-error.test.ts +13 -0
- package/src/utils/navigation.test.ts +2 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/resource-hierarchy.test.ts +402 -15
- package/src/utils/resource-hierarchy.ts +263 -178
- package/src/utils/topology-neighborhood.test.ts +72 -1
- package/src/utils/topology-neighborhood.ts +40 -18
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Boxes, Gauge, ListChecks, Settings } from 'lucide-react'
|
|
2
|
+
import { Badge } from '../../ui/Badge'
|
|
3
|
+
import { AlertBanner, ConditionsSection, Property, PropertyList, ResourceLink, Section, type ConditionTone } from '../../ui/drawer-components'
|
|
4
|
+
import { getAdmissionCheckStatus, getProvisioningRequestStatus, getProvisioningRequestMessage, getProvisioningRequestStatusCondition, isKueueConditionStale } from '../resource-utils-kueue'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
data: any
|
|
8
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AdmissionCheckRenderer({ data, onNavigate }: Props) {
|
|
12
|
+
const spec = data.spec ?? {}
|
|
13
|
+
const condition = data.status?.conditions?.find((c: any) => c.type === 'Active')
|
|
14
|
+
const status = getAdmissionCheckStatus(data)
|
|
15
|
+
const parameters = spec.parameters
|
|
16
|
+
const knownConfig = parameters?.apiGroup === 'kueue.x-k8s.io' && parameters?.kind === 'ProvisioningRequestConfig'
|
|
17
|
+
return <>
|
|
18
|
+
<Section title="Check Controller" icon={ListChecks} defaultExpanded>
|
|
19
|
+
<PropertyList>
|
|
20
|
+
<Property label="Reported State" value={<Badge colorClass={status.color}>{status.text}</Badge>} />
|
|
21
|
+
<Property label="Controller" value={<span className="break-all">{spec.controllerName || 'Not specified'}</span>} />
|
|
22
|
+
{condition?.reason && <Property label="Reason" value={condition.reason} />}
|
|
23
|
+
</PropertyList>
|
|
24
|
+
{condition?.message && <p className="mt-2 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{condition.message}</p>}
|
|
25
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">Active means the controller is ready to evaluate checks. Each Workload reports its own Pending, Ready, Retry or Rejected result.</p>
|
|
26
|
+
{!condition && <p className="mt-2 text-sm text-theme-text-tertiary">Controller readiness has not been reported.</p>}
|
|
27
|
+
{status.text === 'Stale' && <p className="mt-2 text-sm text-warning-text">Reported readiness describes an earlier generation.</p>}
|
|
28
|
+
</Section>
|
|
29
|
+
{parameters && <Section title="Check Configuration" icon={Settings} defaultExpanded>
|
|
30
|
+
<PropertyList>
|
|
31
|
+
<Property label="Kind" value={parameters.kind} />
|
|
32
|
+
<Property label="API Group" value={parameters.apiGroup} />
|
|
33
|
+
<Property label="Name" value={knownConfig && parameters.name ? <ResourceLink name={parameters.name} kind="ProvisioningRequestConfig" group={parameters.apiGroup} onNavigate={onNavigate} /> : parameters.name} />
|
|
34
|
+
</PropertyList>
|
|
35
|
+
</Section>}
|
|
36
|
+
{data.apiVersion === 'kueue.x-k8s.io/v1beta1' && spec.retryDelayMinutes != null && <PropertyList><Property label="Retry Delay (deprecated)" value={`${spec.retryDelayMinutes} minutes`} /></PropertyList>}
|
|
37
|
+
<ConditionsSection conditions={data.status?.conditions} getConditionTone={c => isKueueConditionStale(c, data.metadata?.generation) ? 'unknown' : c.type === 'Active' ? c.status === 'True' ? 'ok' : c.status === 'False' ? 'warning' : 'unknown' : 'unknown'} defaultExpanded />
|
|
38
|
+
</>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function provisioningConditionTone(condition: any): ConditionTone {
|
|
42
|
+
if (condition.status !== 'True' && condition.status !== 'False') return 'unknown'
|
|
43
|
+
if (['Failed', 'CapacityRevoked'].includes(condition.type)) return condition.status === 'True' ? 'fail' : 'ok'
|
|
44
|
+
if (['Accepted', 'Provisioned'].includes(condition.type)) return condition.status === 'True' ? 'ok' : 'unknown'
|
|
45
|
+
return 'unknown'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ProvisioningRequestRenderer({ data, onNavigate }: Props) {
|
|
49
|
+
const spec = data.spec ?? {}
|
|
50
|
+
const status = getProvisioningRequestStatus(data)
|
|
51
|
+
const condition = getProvisioningRequestStatusCondition(data)
|
|
52
|
+
const message = getProvisioningRequestMessage(data)
|
|
53
|
+
const stale = isKueueConditionStale(condition, data.metadata?.generation)
|
|
54
|
+
const namespace = data.metadata?.namespace ?? ''
|
|
55
|
+
const owner = data.metadata?.ownerReferences?.find((ref: any) => ref.controller === true && ref.kind === 'Workload' && ['kueue.x-k8s.io/v1beta1', 'kueue.x-k8s.io/v1beta2'].includes(ref.apiVersion))
|
|
56
|
+
const podSets = spec.podSets ?? []
|
|
57
|
+
return <>
|
|
58
|
+
{stale && <AlertBanner variant="info" title="Reported state describes an earlier generation" message="Inspect the conditions before treating this outcome as current." />}
|
|
59
|
+
<Section title="Provisioning" icon={Gauge} defaultExpanded>
|
|
60
|
+
<PropertyList>
|
|
61
|
+
<Property label="Reported State" value={<Badge colorClass={status.color}>{status.text}</Badge>} />
|
|
62
|
+
<Property label="Provisioning Class" value={<span className="break-all">{spec.provisioningClassName || 'Not specified'}</span>} />
|
|
63
|
+
{condition?.reason && <Property label="Reason" value={condition.reason} />}
|
|
64
|
+
{owner && <Property label="Owner Workload" value={<ResourceLink kind="workloads" group="kueue.x-k8s.io" namespace={namespace} name={owner.name} onNavigate={onNavigate} />} />}
|
|
65
|
+
</PropertyList>
|
|
66
|
+
{message && <p className="mt-2 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{message}</p>}
|
|
67
|
+
{!condition && <p className="mt-2 text-sm text-theme-text-tertiary">No provisioning outcome has been reported. Inspect any conditions below.</p>}
|
|
68
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">Controller-reported provisioning outcome, not a live check of Nodes or running Pods. Accepted means the request was picked up; Provisioned does not mean the Workload is running.</p>
|
|
69
|
+
{condition?.type === 'BookingExpired' && <p className="mt-2 text-xs text-theme-text-tertiary">The capacity booking expired. This alone does not establish a Workload failure; inspect its admission and execution state.</p>}
|
|
70
|
+
</Section>
|
|
71
|
+
<ConditionsSection conditions={data.status?.conditions} getConditionTone={c => isKueueConditionStale(c, data.metadata?.generation) ? 'unknown' : provisioningConditionTone(c)} defaultExpanded />
|
|
72
|
+
<Section title={`Requested Pod Sets (${podSets.length})`} icon={Boxes} defaultExpanded>
|
|
73
|
+
<p className="mb-2 text-xs text-theme-text-tertiary">Requested Pod counts and templates, not observed Pods or allocated devices.</p>
|
|
74
|
+
<div className="space-y-2">
|
|
75
|
+
{podSets.map((set: any, index: number) => <div className="card-inner break-words [&_button]:text-left" key={index}>
|
|
76
|
+
<PropertyList>
|
|
77
|
+
<Property label="Pod Template" value={set.podTemplateRef?.name ? <ResourceLink kind="PodTemplate" group="" namespace={namespace} name={set.podTemplateRef.name} onNavigate={onNavigate} /> : 'Not specified'} />
|
|
78
|
+
<Property label="Requested Pods" value={set.count ?? 'Not specified'} />
|
|
79
|
+
</PropertyList>
|
|
80
|
+
</div>)}
|
|
81
|
+
{podSets.length === 0 && <p className="text-sm text-theme-text-tertiary">No PodSets declared.</p>}
|
|
82
|
+
</div>
|
|
83
|
+
</Section>
|
|
84
|
+
{Object.keys(spec.parameters ?? {}).length > 0 && <Section title="Provider Parameters" icon={Settings} defaultExpanded={false}>
|
|
85
|
+
<PropertyList>{Object.entries(spec.parameters).map(([key, value]) => <Property key={key} label={key} value={<span className="whitespace-pre-wrap break-all">{String(value)}</span>} />)}</PropertyList>
|
|
86
|
+
</Section>}
|
|
87
|
+
{Object.keys(data.status?.provisioningClassDetails ?? {}).length > 0 && <Section title="Provider Details" icon={Settings} defaultExpanded={false}>
|
|
88
|
+
<PropertyList>{Object.entries(data.status.provisioningClassDetails).map(([key, value]) => <Property key={key} label={key} value={<span className="whitespace-pre-wrap break-all">{String(value)}</span>} />)}</PropertyList>
|
|
89
|
+
</Section>}
|
|
90
|
+
</>
|
|
91
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { expect, it, vi } from 'vitest'
|
|
5
|
+
import { ClusterQueueRenderer, LocalQueueRenderer } from './KueueQueueRenderers'
|
|
6
|
+
|
|
7
|
+
it('navigates from a namespaced LocalQueue to exact cluster-scoped Kueue dependencies', async () => {
|
|
8
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
|
9
|
+
const container = document.createElement('div')
|
|
10
|
+
const root = createRoot(container)
|
|
11
|
+
const onNavigate = vi.fn()
|
|
12
|
+
try {
|
|
13
|
+
await act(async () =>
|
|
14
|
+
root.render(
|
|
15
|
+
<LocalQueueRenderer
|
|
16
|
+
data={{
|
|
17
|
+
apiVersion: 'kueue.x-k8s.io/v1beta2',
|
|
18
|
+
metadata: { namespace: 'ml' },
|
|
19
|
+
spec: { clusterQueue: 'shared' },
|
|
20
|
+
}}
|
|
21
|
+
onNavigate={onNavigate}
|
|
22
|
+
/>,
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
const click = async (name: string) => {
|
|
26
|
+
const button = [...container.querySelectorAll('button')].find((button) => button.textContent === name)
|
|
27
|
+
expect(button).toBeDefined()
|
|
28
|
+
await act(async () => button!.click())
|
|
29
|
+
}
|
|
30
|
+
await click('shared')
|
|
31
|
+
expect(onNavigate).toHaveBeenLastCalledWith({
|
|
32
|
+
kind: 'clusterqueues',
|
|
33
|
+
namespace: '',
|
|
34
|
+
name: 'shared',
|
|
35
|
+
group: 'kueue.x-k8s.io',
|
|
36
|
+
})
|
|
37
|
+
await act(async () =>
|
|
38
|
+
root.render(
|
|
39
|
+
<ClusterQueueRenderer
|
|
40
|
+
data={{
|
|
41
|
+
apiVersion: 'kueue.x-k8s.io/v1beta2',
|
|
42
|
+
spec: {
|
|
43
|
+
resourceGroups: [{ flavors: [{ name: 'cpu-flavor', resources: [{ name: 'cpu', nominalQuota: '1' }] }] }],
|
|
44
|
+
admissionChecksStrategy: { admissionChecks: [{ name: 'policy-check' }] },
|
|
45
|
+
},
|
|
46
|
+
}}
|
|
47
|
+
onNavigate={onNavigate}
|
|
48
|
+
/>,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
await click('cpu-flavor')
|
|
52
|
+
expect(onNavigate).toHaveBeenLastCalledWith({
|
|
53
|
+
kind: 'resourceflavors',
|
|
54
|
+
namespace: '',
|
|
55
|
+
name: 'cpu-flavor',
|
|
56
|
+
group: 'kueue.x-k8s.io',
|
|
57
|
+
})
|
|
58
|
+
await click('policy-check')
|
|
59
|
+
expect(onNavigate).toHaveBeenLastCalledWith({
|
|
60
|
+
kind: 'admissionchecks',
|
|
61
|
+
namespace: '',
|
|
62
|
+
name: 'policy-check',
|
|
63
|
+
group: 'kueue.x-k8s.io',
|
|
64
|
+
})
|
|
65
|
+
} finally {
|
|
66
|
+
await act(async () => root.unmount())
|
|
67
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = false
|
|
68
|
+
}
|
|
69
|
+
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { ClusterQueueRenderer, LocalQueueRenderer } from './KueueQueueRenderers'
|
|
4
|
+
import { getAdmissionCheckStatus, getClusterQueueStatus, getLocalQueueStatus, getKueueQueueQuotaRows } from '../resource-utils-kueue'
|
|
5
|
+
|
|
6
|
+
const base = { apiVersion: 'kueue.x-k8s.io/v1beta2', metadata: { generation: 3 }, spec: {} }
|
|
7
|
+
const render = (data: any, local = false) => renderToStaticMarkup(local ? <LocalQueueRenderer data={data} /> : <ClusterQueueRenderer data={data} />)
|
|
8
|
+
const usage = (name: string, resources: Record<string, string>) => ({ name, resources: Object.entries(resources).map(([name, total]) => ({ name, total })) })
|
|
9
|
+
|
|
10
|
+
describe('queue quota accounting', () => {
|
|
11
|
+
it('joins by flavor and resource, preserves declared order, and retains status-only entries', () => {
|
|
12
|
+
const data = { ...base, spec: { resourceGroups: [{ flavors: [
|
|
13
|
+
{ name: 'b', resources: [{ name: 'cpu', nominalQuota: '500m', borrowingLimit: 0 }, { name: 'memory', nominalQuota: '2Gi' }] },
|
|
14
|
+
{ name: 'a', resources: [{ name: 'nvidia.com/gpu', nominalQuota: '0', lendingLimit: '0' }] },
|
|
15
|
+
] }] }, status: {
|
|
16
|
+
flavorsReservation: [usage('a', { 'nvidia.com/gpu': '1' }), usage('b', { memory: '1Gi', cpu: '0' }), usage('old', { cpu: '2' })],
|
|
17
|
+
flavorsUsage: [usage('b', { cpu: '0' })],
|
|
18
|
+
} }
|
|
19
|
+
expect(getKueueQueueQuotaRows(data, true)).toEqual([
|
|
20
|
+
{ flavor: 'b', resource: 'cpu', configured: true, nominal: '500m', borrowingLimit: 0, reserved: '0', used: '0' },
|
|
21
|
+
{ flavor: 'b', resource: 'memory', configured: true, nominal: '2Gi', reserved: '1Gi' },
|
|
22
|
+
{ flavor: 'a', resource: 'nvidia.com/gpu', configured: true, nominal: '0', lendingLimit: '0', reserved: '1' },
|
|
23
|
+
{ flavor: 'old', resource: 'cpu', configured: false, reserved: '2' },
|
|
24
|
+
])
|
|
25
|
+
expect(render(data)).toContain('Not in current configuration')
|
|
26
|
+
expect(render(data)).toContain('Not reported')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('keeps borrowed amounts inside totals and distinguishes zero limits from uncapped borrowing', () => {
|
|
30
|
+
const data = { ...base, spec: { cohortName: 'shared', resourceGroups: [{ flavors: [{ name: 'cpu', resources: [
|
|
31
|
+
{ name: 'cpu', nominalQuota: '1', borrowingLimit: '0', lendingLimit: 0 }, { name: 'memory', nominalQuota: '1Gi' },
|
|
32
|
+
] }] }] }, status: { flavorsReservation: [{ name: 'cpu', resources: [{ name: 'cpu', total: '2', borrowed: '1' }] }] } }
|
|
33
|
+
const html = render(data)
|
|
34
|
+
for (const text of ['Borrowing limit: 0', 'Lending limit: 0', 'borrowing has no configured cap', 'all nominal quota may be lent', 'Borrowed reservation: 1']) expect(html).toContain(text)
|
|
35
|
+
expect(getKueueQueueQuotaRows(data, true)[0].reserved).toBe('2')
|
|
36
|
+
expect(render({ ...data, spec: { ...data.spec, cohortName: undefined } })).not.toContain('Borrowing limit:')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reads the actual v1beta1 LocalQueue usage spelling without importing parent quota', () => {
|
|
40
|
+
const data = { ...base, apiVersion: 'kueue.x-k8s.io/v1beta1', spec: { clusterQueue: 'shared' }, status: { flavorUsage: [usage('cpu', { cpu: '50m' })] } }
|
|
41
|
+
expect(getKueueQueueQuotaRows(data, false)[0].used).toBe('50m')
|
|
42
|
+
expect(render(data, true)).toContain('50m')
|
|
43
|
+
expect(render(data, true)).not.toContain('Nominal')
|
|
44
|
+
expect(getKueueQueueQuotaRows({ ...data, apiVersion: base.apiVersion }, false)).toEqual([])
|
|
45
|
+
expect(getKueueQueueQuotaRows({ ...data, apiVersion: base.apiVersion, status: { flavorsUsage: data.status.flavorUsage } }, false)[0].used).toBe('50m')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('queue status and policy', () => {
|
|
50
|
+
it('does not turn absent status into zero workloads or healthy state', () => {
|
|
51
|
+
const html = render(base)
|
|
52
|
+
expect(html).toContain('Controller status has not been reported')
|
|
53
|
+
expect(html).not.toContain('No Active condition reported')
|
|
54
|
+
expect(html).toContain('Unknown')
|
|
55
|
+
const counted = render({ ...base, status: { pendingWorkloads: 0, reservingWorkloads: 1, admittedWorkloads: 1 } })
|
|
56
|
+
expect(counted).not.toContain('counts have not been reported')
|
|
57
|
+
expect(counted).toContain('not additive')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('keeps observed stops neutral, unrelated AdmissionChecks unchanged, and stale evidence unknown', () => {
|
|
61
|
+
const stopped = { ...base, status: { conditions: [{ type: 'Active', status: 'False', reason: 'Stopped', observedGeneration: 3 }] } }
|
|
62
|
+
expect(getClusterQueueStatus(stopped).level).toBe('neutral')
|
|
63
|
+
expect(getLocalQueueStatus(stopped).level).toBe('neutral')
|
|
64
|
+
expect(getAdmissionCheckStatus(stopped).level).toBe('alert')
|
|
65
|
+
expect(render(stopped)).not.toContain('failing')
|
|
66
|
+
const stale = { ...stopped, metadata: { generation: 4 } }
|
|
67
|
+
expect(getClusterQueueStatus(stale).level).toBe('unknown')
|
|
68
|
+
expect(render(stale)).toContain('earlier generation')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('does not claim a parent is held based on LocalQueue inactivity', () => {
|
|
72
|
+
const data = { ...base, spec: { clusterQueue: 'parent' }, status: { conditions: [{ type: 'Active', status: 'False', reason: 'ClusterQueueIsInactive', message: 'parent inactive' }] } }
|
|
73
|
+
expect(getLocalQueueStatus(data).level).toBe('alert')
|
|
74
|
+
expect(render(data, true)).toContain('Inspect the ClusterQueue')
|
|
75
|
+
expect(render(data, true)).not.toContain('Configured to stop')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it.each(['Hold', 'HoldAndDrain'])('separates requested %s from an observed failure and explains reservations', (stopPolicy) => {
|
|
79
|
+
const data = { ...base, spec: { stopPolicy }, status: { conditions: [{ type: 'Active', status: 'False', reason: 'FlavorNotFound', message: 'flavor missing' }] } }
|
|
80
|
+
expect(getClusterQueueStatus(data).text).toBe('FlavorNotFound')
|
|
81
|
+
const html = render(data)
|
|
82
|
+
expect(html).toContain('cancel reservations')
|
|
83
|
+
expect(html).toContain(stopPolicy === 'Hold' ? 'may continue' : 'are to be evicted')
|
|
84
|
+
expect(html).toContain('flavor missing')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('distinguishes no namespaces eligible from all and from a selector', () => {
|
|
88
|
+
expect(render(base)).toContain('No namespaces eligible')
|
|
89
|
+
expect(render({ ...base, spec: { namespaceSelector: {} } })).toContain('All namespaces')
|
|
90
|
+
expect(render({ ...base, spec: { namespaceSelector: { matchLabels: { team: 'ml' } } } })).toContain('team=ml')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('preserves v1beta1 cohort and both check declarations with flavor applicability', () => {
|
|
94
|
+
const html = render({ ...base, apiVersion: 'kueue.x-k8s.io/v1beta1', spec: {
|
|
95
|
+
cohort: 'legacy-cohort', admissionChecks: ['all-check'],
|
|
96
|
+
admissionChecksStrategy: { admissionChecks: [{ name: 'gpu-check', onFlavors: ['a100'] }] },
|
|
97
|
+
} })
|
|
98
|
+
for (const text of ['legacy-cohort', 'all-check', 'gpu-check', 'Applies to all flavors', 'Applies to flavors: a100']) expect(html).toContain(text)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
it('omits zero borrowing and irrelevant cohort policies on a real-shaped standalone queue', () => {
|
|
104
|
+
const data = { ...base, spec: {
|
|
105
|
+
resourceGroups: [{ flavors: [{ name: 'cpu', resources: [{ name: 'cpu', nominalQuota: '2' }] }] }],
|
|
106
|
+
preemption: { reclaimWithinCohort: 'Never', borrowWithinCohort: { policy: 'Never' } },
|
|
107
|
+
flavorFungibility: { whenCanBorrow: 'MayStopSearch' },
|
|
108
|
+
}, status: {
|
|
109
|
+
flavorsReservation: [{ name: 'cpu', resources: [{ name: 'cpu', total: '0', borrowed: '0' }] }],
|
|
110
|
+
flavorsUsage: [{ name: 'cpu', resources: [{ name: 'cpu', total: '0', borrowed: '0' }] }],
|
|
111
|
+
} }
|
|
112
|
+
const html = render(data)
|
|
113
|
+
expect(html).not.toContain('Nominal quota is not a hard ceiling')
|
|
114
|
+
for (const text of ['Borrowed reservation:', 'Reclaim within Cohort', 'Borrow with Preemption', 'Flavor when Borrowing']) expect(html).not.toContain(text)
|
|
115
|
+
const cohort = render({ ...data, spec: { ...data.spec, cohortName: 'shared' } })
|
|
116
|
+
expect(cohort).toContain('Nominal quota is not a hard ceiling')
|
|
117
|
+
expect(cohort).toContain('borrowing has no configured cap')
|
|
118
|
+
expect(cohort).not.toContain('Borrowing limit:')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('uses the Active label consistently for stale healthy evidence', () => {
|
|
122
|
+
expect(getClusterQueueStatus({ ...base, status: { conditions: [{ type: 'Active', status: 'True', reason: 'Ready', observedGeneration: 2 }] } }).text).toBe('Active (stale)')
|
|
123
|
+
})
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Gauge } from 'lucide-react'
|
|
2
|
+
import { Badge } from '../../ui/Badge'
|
|
3
|
+
import {
|
|
4
|
+
AlertBanner,
|
|
5
|
+
ConditionsSection,
|
|
6
|
+
LabelSelectorDisplay,
|
|
7
|
+
Property,
|
|
8
|
+
PropertyList,
|
|
9
|
+
ResourceLink,
|
|
10
|
+
Section,
|
|
11
|
+
type ConditionTone,
|
|
12
|
+
} from '../../ui/drawer-components'
|
|
13
|
+
import {
|
|
14
|
+
getClusterQueueCohort,
|
|
15
|
+
getClusterQueueStatus,
|
|
16
|
+
getLocalQueueStatus,
|
|
17
|
+
getKueueQueueCondition,
|
|
18
|
+
getKueueQueueQuotaRows,
|
|
19
|
+
isKueueConditionStale,
|
|
20
|
+
type KueueQuotaRow,
|
|
21
|
+
} from '../resource-utils-kueue'
|
|
22
|
+
|
|
23
|
+
const KUEUE_GROUP = 'kueue.x-k8s.io'
|
|
24
|
+
interface QueueRendererProps {
|
|
25
|
+
data: any
|
|
26
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const quantity = (value: string | number | undefined | null) => (value == null ? 'Not reported' : String(value))
|
|
30
|
+
|
|
31
|
+
function QueueStatus({ data, local = false }: Pick<QueueRendererProps, 'data'> & { local?: boolean }) {
|
|
32
|
+
const condition = getKueueQueueCondition(data)
|
|
33
|
+
const badge = local ? getLocalQueueStatus(data) : getClusterQueueStatus(data)
|
|
34
|
+
const stale = isKueueConditionStale(condition, data.metadata?.generation)
|
|
35
|
+
const stop = data.spec?.stopPolicy ?? 'None'
|
|
36
|
+
const counts = [
|
|
37
|
+
['Pending', data.status?.pendingWorkloads],
|
|
38
|
+
['Reserving quota', data.status?.reservingWorkloads],
|
|
39
|
+
['Admitted', data.status?.admittedWorkloads],
|
|
40
|
+
] as const
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{stale && (
|
|
44
|
+
<AlertBanner
|
|
45
|
+
variant="info"
|
|
46
|
+
title="Reported state describes an earlier generation"
|
|
47
|
+
message={`Active condition: generation ${condition.observedGeneration}; queue: generation ${data.metadata.generation}. The reported state may not reflect the current specification.`}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
<Section title="Queue Status" icon={Gauge}>
|
|
51
|
+
<PropertyList>
|
|
52
|
+
<Property label="Reported State" value={<Badge colorClass={badge.color}>{badge.text}</Badge>} />
|
|
53
|
+
<Property label="Stop Policy" value={stop} />
|
|
54
|
+
{counts.some(([, count]) => count != null) &&
|
|
55
|
+
counts.map(([label, count]) => <Property key={label} label={label} value={quantity(count)} />)}
|
|
56
|
+
</PropertyList>
|
|
57
|
+
{condition?.message && (
|
|
58
|
+
<p className="mt-2 whitespace-pre-wrap break-words text-sm text-theme-text-secondary">{condition.message}</p>
|
|
59
|
+
)}
|
|
60
|
+
{!condition && counts.some(([, count]) => count != null) && (
|
|
61
|
+
<p className="mt-2 text-sm text-theme-text-secondary">No Active condition reported.</p>
|
|
62
|
+
)}
|
|
63
|
+
{counts.every(([, count]) => count == null) ? (
|
|
64
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">
|
|
65
|
+
{condition ? 'Workload counts have not been reported.' : 'Controller status has not been reported.'}
|
|
66
|
+
</p>
|
|
67
|
+
) : (
|
|
68
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">
|
|
69
|
+
Reserving counts include admitted Workloads; these counts are not additive. Admission does not establish
|
|
70
|
+
that Pods are running.
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
73
|
+
{(stop === 'Hold' || stop === 'HoldAndDrain') && (
|
|
74
|
+
<p className="mt-2 text-sm text-theme-text-secondary">
|
|
75
|
+
Configured to stop new reservations and cancel reservations for Workloads awaiting admission.{' '}
|
|
76
|
+
{stop === 'HoldAndDrain'
|
|
77
|
+
? 'Admitted Workloads are to be evicted.'
|
|
78
|
+
: 'Already admitted Workloads may continue.'}{' '}
|
|
79
|
+
The reported state above shows the controller observation.
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
</Section>
|
|
83
|
+
</>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function QueueConditions({ data }: Pick<QueueRendererProps, 'data'>) {
|
|
88
|
+
const tone = (condition: any): ConditionTone => {
|
|
89
|
+
if (isKueueConditionStale(condition, data.metadata?.generation) || condition.type !== 'Active') return 'unknown'
|
|
90
|
+
if (condition.status === 'True') return 'ok'
|
|
91
|
+
if (condition.status === 'False') return condition.reason === 'Stopped' ? 'unknown' : 'warning'
|
|
92
|
+
return 'unknown'
|
|
93
|
+
}
|
|
94
|
+
return <ConditionsSection conditions={data.status?.conditions} getConditionTone={tone} defaultExpanded />
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function QuotaAccounting({ data, clusterQueue = false, onNavigate }: QueueRendererProps & { clusterQueue?: boolean }) {
|
|
98
|
+
const rows = getKueueQueueQuotaRows(data, clusterQueue)
|
|
99
|
+
const flavors = new Map<string, KueueQuotaRow[]>()
|
|
100
|
+
for (const row of rows) {
|
|
101
|
+
const entries = flavors.get(row.flavor) ?? []
|
|
102
|
+
entries.push(row)
|
|
103
|
+
flavors.set(row.flavor, entries)
|
|
104
|
+
}
|
|
105
|
+
const hasCohort = clusterQueue && getClusterQueueCohort(data) !== '-'
|
|
106
|
+
return (
|
|
107
|
+
<Section title={clusterQueue ? 'Quota by Flavor' : 'Quota Accounting'}>
|
|
108
|
+
<p className="mb-3 text-xs text-theme-text-tertiary">
|
|
109
|
+
Reserved and admitted usage are quota accounting from resource requests, not measured utilization. Admitted
|
|
110
|
+
usage is included in reservations.
|
|
111
|
+
{hasCohort && ' Nominal quota is not a hard ceiling when borrowing is available.'}
|
|
112
|
+
</p>
|
|
113
|
+
{rows.length === 0 ? (
|
|
114
|
+
<p className="text-sm text-theme-text-secondary">
|
|
115
|
+
{clusterQueue ? 'No quota configuration or accounting reported.' : 'No quota accounting reported.'}
|
|
116
|
+
</p>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="space-y-4">
|
|
119
|
+
{[...flavors].map(([flavor, entries]) => (
|
|
120
|
+
<div key={flavor} className="min-w-0">
|
|
121
|
+
<div className="mb-2 break-all text-sm font-medium [&_button]:text-left">
|
|
122
|
+
<ResourceLink name={flavor} kind="resourceflavors" group={KUEUE_GROUP} onNavigate={onNavigate} />
|
|
123
|
+
</div>
|
|
124
|
+
<table className="w-full table-fixed text-left text-xs">
|
|
125
|
+
<thead className="text-theme-text-tertiary">
|
|
126
|
+
<tr>
|
|
127
|
+
<th className="w-[32%] py-2 pr-2 font-medium">Resource</th>
|
|
128
|
+
{clusterQueue && <th className="px-2 py-2 font-medium">Nominal</th>}
|
|
129
|
+
<th className="px-2 py-2 font-medium">Reserved</th>
|
|
130
|
+
<th className="py-2 pl-2 font-medium">Admitted usage</th>
|
|
131
|
+
</tr>
|
|
132
|
+
</thead>
|
|
133
|
+
<tbody>
|
|
134
|
+
{entries.map((row) => (
|
|
135
|
+
<QuotaRow key={row.resource} row={row} clusterQueue={clusterQueue} hasCohort={hasCohort} />
|
|
136
|
+
))}
|
|
137
|
+
</tbody>
|
|
138
|
+
</table>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</Section>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function QuotaRow({ row, clusterQueue, hasCohort }: { row: KueueQuotaRow; clusterQueue: boolean; hasCohort: boolean }) {
|
|
148
|
+
const borrowed = [row.borrowedReservation, row.borrowedUsage].some((value) => value != null && String(value) !== '0')
|
|
149
|
+
const limits = hasCohort && row.configured && (row.borrowingLimit != null || row.lendingLimit != null)
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<tr className="border-t border-theme-border align-top">
|
|
153
|
+
<td className="break-all py-2 pr-2 font-medium text-theme-text-primary">
|
|
154
|
+
{row.resource}
|
|
155
|
+
{clusterQueue && !row.configured && (
|
|
156
|
+
<span className="mt-1 block text-theme-text-tertiary font-normal">Not in current configuration</span>
|
|
157
|
+
)}
|
|
158
|
+
</td>
|
|
159
|
+
{clusterQueue && (
|
|
160
|
+
<td className="break-all px-2 py-2 font-mono text-theme-text-secondary">{quantity(row.nominal)}</td>
|
|
161
|
+
)}
|
|
162
|
+
<td className="break-all px-2 py-2 font-mono text-theme-text-secondary">{quantity(row.reserved)}</td>
|
|
163
|
+
<td className="break-all py-2 pl-2 font-mono text-theme-text-secondary">{quantity(row.used)}</td>
|
|
164
|
+
</tr>
|
|
165
|
+
{clusterQueue && (limits || borrowed) && (
|
|
166
|
+
<tr>
|
|
167
|
+
<td colSpan={4} className="pb-3 text-theme-text-tertiary">
|
|
168
|
+
{limits && (
|
|
169
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
170
|
+
{row.borrowingLimit != null && <span>Borrowing limit: {String(row.borrowingLimit)}</span>}
|
|
171
|
+
{row.lendingLimit != null && <span>Lending limit: {String(row.lendingLimit)}</span>}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
{borrowed && (
|
|
175
|
+
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1">
|
|
176
|
+
<span>Borrowed reservation: {quantity(row.borrowedReservation)}</span>
|
|
177
|
+
<span>Borrowed admitted usage: {quantity(row.borrowedUsage)}</span>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</td>
|
|
181
|
+
</tr>
|
|
182
|
+
)}
|
|
183
|
+
</>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function LocalQueueRenderer({ data, onNavigate }: QueueRendererProps) {
|
|
188
|
+
const parentInactive = getKueueQueueCondition(data)?.reason === 'ClusterQueueIsInactive'
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<QueueStatus data={data} local />
|
|
192
|
+
<Section title="Cluster Queue">
|
|
193
|
+
{data.spec?.clusterQueue ? (
|
|
194
|
+
<ResourceLink
|
|
195
|
+
name={data.spec.clusterQueue}
|
|
196
|
+
kind="clusterqueues"
|
|
197
|
+
group={KUEUE_GROUP}
|
|
198
|
+
onNavigate={onNavigate}
|
|
199
|
+
/>
|
|
200
|
+
) : (
|
|
201
|
+
<span className="text-sm text-theme-text-secondary">No ClusterQueue reference reported.</span>
|
|
202
|
+
)}
|
|
203
|
+
{parentInactive && (
|
|
204
|
+
<p className="mt-2 text-sm text-theme-text-secondary">
|
|
205
|
+
Inspect the ClusterQueue for the cause of its reported inactivity.
|
|
206
|
+
</p>
|
|
207
|
+
)}
|
|
208
|
+
</Section>
|
|
209
|
+
<QuotaAccounting data={data} onNavigate={onNavigate} />
|
|
210
|
+
<QueueConditions data={data} />
|
|
211
|
+
</>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function ClusterQueueRenderer({ data, onNavigate }: QueueRendererProps) {
|
|
216
|
+
const spec = data.spec ?? {}
|
|
217
|
+
const cohort = getClusterQueueCohort(data)
|
|
218
|
+
const checks: { name: string; onFlavors?: string[] }[] = [
|
|
219
|
+
...(data.apiVersion === 'kueue.x-k8s.io/v1beta1'
|
|
220
|
+
? (spec.admissionChecks ?? []).map((name: string) => ({ name }))
|
|
221
|
+
: []),
|
|
222
|
+
...(spec.admissionChecksStrategy?.admissionChecks ?? []),
|
|
223
|
+
]
|
|
224
|
+
return (
|
|
225
|
+
<>
|
|
226
|
+
<QueueStatus data={data} />
|
|
227
|
+
<QuotaAccounting data={data} clusterQueue onNavigate={onNavigate} />
|
|
228
|
+
<Section title="Admission Policy">
|
|
229
|
+
<PropertyList>
|
|
230
|
+
<Property
|
|
231
|
+
label="Eligible Namespaces"
|
|
232
|
+
value={
|
|
233
|
+
<LabelSelectorDisplay
|
|
234
|
+
selector={spec.namespaceSelector}
|
|
235
|
+
emptyText={spec.namespaceSelector == null ? 'No namespaces eligible' : 'All namespaces'}
|
|
236
|
+
/>
|
|
237
|
+
}
|
|
238
|
+
/>
|
|
239
|
+
<Property label="Queueing Strategy" value={spec.queueingStrategy ?? 'BestEffortFIFO'} />
|
|
240
|
+
<Property label="Cohort" value={cohort === '-' ? 'None' : cohort} />
|
|
241
|
+
{spec.preemption?.withinClusterQueue != null && (
|
|
242
|
+
<Property label="Preemption within Queue" value={spec.preemption.withinClusterQueue} />
|
|
243
|
+
)}
|
|
244
|
+
{cohort !== '-' && spec.preemption?.reclaimWithinCohort != null && (
|
|
245
|
+
<Property label="Reclaim within Cohort" value={spec.preemption.reclaimWithinCohort} />
|
|
246
|
+
)}
|
|
247
|
+
{cohort !== '-' && spec.preemption?.borrowWithinCohort?.policy != null && (
|
|
248
|
+
<Property label="Borrow with Preemption" value={spec.preemption.borrowWithinCohort.policy} />
|
|
249
|
+
)}
|
|
250
|
+
{cohort !== '-' && spec.preemption?.borrowWithinCohort?.maxPriorityThreshold != null && (
|
|
251
|
+
<Property
|
|
252
|
+
label="Preemption Priority Threshold"
|
|
253
|
+
value={spec.preemption.borrowWithinCohort.maxPriorityThreshold}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
{cohort !== '-' && spec.flavorFungibility?.whenCanBorrow != null && (
|
|
257
|
+
<Property label="Flavor when Borrowing" value={spec.flavorFungibility.whenCanBorrow} />
|
|
258
|
+
)}
|
|
259
|
+
{spec.flavorFungibility?.whenCanPreempt != null && (
|
|
260
|
+
<Property label="Flavor when Preempting" value={spec.flavorFungibility.whenCanPreempt} />
|
|
261
|
+
)}
|
|
262
|
+
{spec.flavorFungibility?.preference != null && (
|
|
263
|
+
<Property label="Flavor Preference" value={spec.flavorFungibility.preference} />
|
|
264
|
+
)}
|
|
265
|
+
{spec.fairSharing?.weight != null && (
|
|
266
|
+
<Property label="Fair Sharing Weight" value={String(spec.fairSharing.weight)} />
|
|
267
|
+
)}
|
|
268
|
+
{data.status?.fairSharing?.weightedShare != null && (
|
|
269
|
+
<Property label="Reported Weighted Share" value={String(data.status.fairSharing.weightedShare)} />
|
|
270
|
+
)}
|
|
271
|
+
</PropertyList>
|
|
272
|
+
{cohort !== '-' && (
|
|
273
|
+
<p className="mt-2 text-xs text-theme-text-tertiary">
|
|
274
|
+
Absent an explicit limit, borrowing has no configured cap and all nominal quota may be lent. Borrowing
|
|
275
|
+
depends on available cohort quota; a borrowing limit does not guarantee capacity. Reported borrowed amounts
|
|
276
|
+
are already included in their totals.
|
|
277
|
+
</p>
|
|
278
|
+
)}
|
|
279
|
+
</Section>
|
|
280
|
+
{checks.length > 0 && (
|
|
281
|
+
<Section title="Required Admission Checks">
|
|
282
|
+
<div className="space-y-3">
|
|
283
|
+
{checks.map((check, index) => (
|
|
284
|
+
<div key={`${check.name}-${index}`} className="text-sm">
|
|
285
|
+
<ResourceLink name={check.name} kind="admissionchecks" group={KUEUE_GROUP} onNavigate={onNavigate} />
|
|
286
|
+
<p className="mt-1 break-words text-xs text-theme-text-secondary">
|
|
287
|
+
{check.onFlavors?.length
|
|
288
|
+
? `Applies to flavors: ${check.onFlavors.join(', ')}`
|
|
289
|
+
: 'Applies to all flavors'}
|
|
290
|
+
</p>
|
|
291
|
+
</div>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
</Section>
|
|
295
|
+
)}
|
|
296
|
+
<QueueConditions data={data} />
|
|
297
|
+
</>
|
|
298
|
+
)
|
|
299
|
+
}
|