@skyhook-io/k8s-ui 1.7.11 → 1.7.12
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 +1 -1
- package/src/components/dock/TerminalTab.tsx +4 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
- package/src/components/issues/IssuesView.tsx +64 -17
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +4 -4
- package/src/components/issues/severity.ts +5 -0
- package/src/components/issues/types.ts +43 -0
- package/src/components/logs/LogCore.tsx +13 -2
- package/src/components/logs/LogToolbarSelects.tsx +6 -2
- package/src/components/logs/LogsViewer.tsx +66 -10
- package/src/components/logs/WorkloadLogsViewer.tsx +60 -8
- package/src/components/logs/useLogStream.ts +41 -3
- package/src/components/resources/ResourcesView.tsx +550 -52
- package/src/components/resources/column-filter-serialization.test.ts +26 -0
- package/src/components/resources/get-default-container-name.test.ts +31 -0
- package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
- package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
- package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
- package/src/components/resources/renderers/PodRenderer.tsx +2 -2
- package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
- package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
- package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
- package/src/components/resources/renderers/dra-cells.tsx +80 -0
- package/src/components/resources/renderers/index.ts +8 -0
- package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
- package/src/components/resources/resource-utils-dra.ts +90 -0
- package/src/components/resources/resource-utils-nvidia.ts +63 -0
- package/src/components/resources/resource-utils.ts +62 -4
- package/src/components/shared/ResourceActionsBar.tsx +5 -4
- package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
- package/src/components/ui/ConfirmDialog.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +23 -1
- package/src/types/core.ts +1 -0
- package/src/utils/api-resources.ts +21 -0
- package/src/utils/custom-columns.test.ts +111 -0
- package/src/utils/custom-columns.ts +49 -0
- package/src/utils/extended-resources.test.ts +152 -0
- package/src/utils/extended-resources.ts +121 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// DRA (Dynamic Resource Allocation, resource.k8s.io) utility functions.
|
|
2
|
+
// Accessors tolerate all served shapes: v1/v1beta2 nest device-class refs
|
|
3
|
+
// under requests[].exactly (or firstAvailable subrequests); v1beta1 had
|
|
4
|
+
// deviceClassName directly on the request.
|
|
5
|
+
|
|
6
|
+
import type { StatusBadge } from './resource-utils'
|
|
7
|
+
import { healthColors } from './resource-utils'
|
|
8
|
+
|
|
9
|
+
function requestDeviceClasses(requests: any[]): string[] {
|
|
10
|
+
const seen = new Set<string>()
|
|
11
|
+
for (const req of requests || []) {
|
|
12
|
+
const direct = req?.exactly?.deviceClassName || req?.deviceClassName
|
|
13
|
+
if (direct) seen.add(direct)
|
|
14
|
+
for (const sub of req?.firstAvailable || []) {
|
|
15
|
+
if (sub?.deviceClassName) seen.add(sub.deviceClassName)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...seen]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getResourceClaimDeviceClasses(resource: any): string[] {
|
|
22
|
+
return requestDeviceClasses(resource?.spec?.devices?.requests)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getResourceClaimTemplateDeviceClasses(resource: any): string[] {
|
|
26
|
+
return requestDeviceClasses(resource?.spec?.spec?.devices?.requests)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getResourceClaimAllocation(resource: any): { driver: string; pool: string; device: string }[] {
|
|
30
|
+
const results = resource?.status?.allocation?.devices?.results || []
|
|
31
|
+
return results.map((r: any) => ({
|
|
32
|
+
driver: r?.driver || '',
|
|
33
|
+
pool: r?.pool || '',
|
|
34
|
+
device: r?.device || '',
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getResourceClaimReservedFor(resource: any): { resource: string; name: string }[] {
|
|
39
|
+
return (resource?.status?.reservedFor || []).map((r: any) => ({
|
|
40
|
+
resource: r?.resource || r?.apiGroup || '',
|
|
41
|
+
name: r?.name || '',
|
|
42
|
+
}))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getResourceClaimStatus(resource: any): StatusBadge {
|
|
46
|
+
// Allocated means devices were actually assigned — keyed to devices.results
|
|
47
|
+
// so the badge always agrees with the Allocation section.
|
|
48
|
+
const allocated = (resource?.status?.allocation?.devices?.results || []).length > 0
|
|
49
|
+
const reserved = (resource?.status?.reservedFor || []).length > 0
|
|
50
|
+
if (allocated && reserved) {
|
|
51
|
+
return { text: 'Allocated', color: healthColors.healthy, level: 'healthy' }
|
|
52
|
+
}
|
|
53
|
+
if (allocated) {
|
|
54
|
+
// Allocated-but-unreserved is normal transiently; long-lived it leaks a device
|
|
55
|
+
return { text: 'Unreserved', color: healthColors.degraded, level: 'degraded' }
|
|
56
|
+
}
|
|
57
|
+
return { text: 'Pending', color: healthColors.unknown, level: 'unknown' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getResourceClaimTemplateStatus(_resource: any): StatusBadge {
|
|
61
|
+
return { text: 'Template', color: healthColors.neutral, level: 'neutral' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getDeviceClassStatus(_resource: any): StatusBadge {
|
|
65
|
+
return { text: 'Available', color: healthColors.neutral, level: 'neutral' }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getDeviceClassSelectorCount(resource: any): number {
|
|
69
|
+
return (resource?.spec?.selectors || []).length
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getResourceSliceStatus(_resource: any): StatusBadge {
|
|
73
|
+
return { text: 'Published', color: healthColors.neutral, level: 'neutral' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getResourceSliceDriver(resource: any): string {
|
|
77
|
+
return resource?.spec?.driver || '-'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getResourceSlicePool(resource: any): string {
|
|
81
|
+
return resource?.spec?.pool?.name || '-'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getResourceSliceNode(resource: any): string {
|
|
85
|
+
return resource?.spec?.nodeName || ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getResourceSliceDeviceCount(resource: any): number {
|
|
89
|
+
return (resource?.spec?.devices || []).length
|
|
90
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// NVIDIA GPU Operator (nvidia.com) utility functions.
|
|
2
|
+
// ClusterPolicy here is nvidia.com's — distinct from Kyverno's ClusterPolicy;
|
|
3
|
+
// dispatch call sites guard on apiVersion before reaching these.
|
|
4
|
+
|
|
5
|
+
import type { StatusBadge } from './resource-utils'
|
|
6
|
+
import { healthColors } from './resource-utils'
|
|
7
|
+
|
|
8
|
+
function stateBadge(state: string | undefined): StatusBadge {
|
|
9
|
+
switch (state) {
|
|
10
|
+
case 'ready':
|
|
11
|
+
return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
|
|
12
|
+
case 'notReady':
|
|
13
|
+
return { text: 'Not Ready', color: healthColors.alert, level: 'alert' }
|
|
14
|
+
case 'disabled':
|
|
15
|
+
return { text: 'Disabled', color: healthColors.neutral, level: 'neutral' }
|
|
16
|
+
default:
|
|
17
|
+
return { text: state || 'Unknown', color: healthColors.unknown, level: 'unknown' }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getNvidiaClusterPolicyStatus(resource: any): StatusBadge {
|
|
22
|
+
return stateBadge(resource?.status?.state)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const NVIDIA_CLUSTER_POLICY_COMPONENTS = [
|
|
26
|
+
{ key: 'driver', label: 'Driver' },
|
|
27
|
+
{ key: 'toolkit', label: 'Container Toolkit' },
|
|
28
|
+
{ key: 'devicePlugin', label: 'Device Plugin' },
|
|
29
|
+
{ key: 'dcgmExporter', label: 'DCGM Exporter' },
|
|
30
|
+
{ key: 'dcgm', label: 'DCGM' },
|
|
31
|
+
{ key: 'gfd', label: 'GPU Feature Discovery' },
|
|
32
|
+
{ key: 'migManager', label: 'MIG Manager' },
|
|
33
|
+
{ key: 'nodeStatusExporter', label: 'Node Status Exporter' },
|
|
34
|
+
{ key: 'vgpuManager', label: 'vGPU Manager' },
|
|
35
|
+
{ key: 'gds', label: 'GPUDirect Storage' },
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
export function getNvidiaClusterPolicyEnabledComponents(resource: any): { label: string; enabled: boolean }[] {
|
|
39
|
+
const spec = resource?.spec || {}
|
|
40
|
+
return NVIDIA_CLUSTER_POLICY_COMPONENTS
|
|
41
|
+
.filter(({ key }) => spec[key]?.enabled !== undefined)
|
|
42
|
+
.map(({ key, label }) => ({ label, enabled: !!spec[key].enabled }))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getNvidiaClusterPolicyMigStrategy(resource: any): string {
|
|
46
|
+
return resource?.spec?.mig?.strategy || '-'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getNvidiaDriverStatus(resource: any): StatusBadge {
|
|
50
|
+
return stateBadge(resource?.status?.state)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getNvidiaDriverType(resource: any): string {
|
|
54
|
+
return resource?.spec?.driverType || '-'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getNvidiaDriverVersion(resource: any): string {
|
|
58
|
+
return resource?.spec?.version || '-'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isNvidiaResource(resource: any): boolean {
|
|
62
|
+
return !!resource?.apiVersion?.startsWith('nvidia.com/')
|
|
63
|
+
}
|
|
@@ -10,6 +10,8 @@ import { getScaledObjectStatus, getScaledJobStatus } from './resource-utils-keda
|
|
|
10
10
|
import { getGitRepositoryStatus, getOCIRepositoryStatus, getHelmRepositoryStatus, getHelmRepositoryType, getKustomizationStatus, getFluxHelmReleaseStatus, getFluxAlertStatus } from './resource-utils-flux'
|
|
11
11
|
import { getArgoApplicationStatus, getArgoApplicationSetStatus, getArgoApplicationSync, getArgoApplicationHealth, getArgoApplicationProject } from './resource-utils-argo'
|
|
12
12
|
import { getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
|
|
13
|
+
import { getResourceClaimStatus as _getResourceClaimStatus, getResourceClaimDeviceClasses as _getResourceClaimDeviceClasses, getResourceClaimTemplateDeviceClasses as _getResourceClaimTemplateDeviceClasses, getResourceClaimAllocation as _getResourceClaimAllocation, getResourceClaimReservedFor as _getResourceClaimReservedFor } from './resource-utils-dra'
|
|
14
|
+
import { getNvidiaClusterPolicyStatus as _getNvidiaClusterPolicyStatus, getNvidiaClusterPolicyEnabledComponents as _getNvidiaClusterPolicyEnabledComponents, getNvidiaDriverStatus as _getNvidiaDriverStatus } from './resource-utils-nvidia'
|
|
13
15
|
import { getBackupStatus as _getBackupStatus, getRestoreStatus as _getRestoreStatus, getScheduleStatus as _getScheduleStatus, getBSLStatus as _getBSLStatus } from './resource-utils-velero'
|
|
14
16
|
import { getExternalSecretStatus as _getExternalSecretStatus, getClusterExternalSecretStatus as _getClusterExternalSecretStatus, getSecretStoreStatus as _getSecretStoreStatus, getClusterSecretStoreStatus as _getClusterSecretStoreStatus, getSecretStoreProviderType as _getSecretStoreProviderType } from './resource-utils-eso'
|
|
15
17
|
|
|
@@ -418,6 +420,23 @@ export interface ContainerSquareState {
|
|
|
418
420
|
}
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
/**
|
|
424
|
+
* The container to default to for exec / logs on a multi-container pod. Honors
|
|
425
|
+
* the kubectl.kubernetes.io/default-container annotation (the convention
|
|
426
|
+
* kubectl, k9s, and Lens follow, and what service meshes like Istio set to
|
|
427
|
+
* point past their injected sidecar), falling back to the first container.
|
|
428
|
+
* Without this, mesh-injected pods default to their distroless sidecar — which
|
|
429
|
+
* has no shell — making the terminal appear broken.
|
|
430
|
+
*/
|
|
431
|
+
export function getDefaultContainerName(pod: any): string | undefined {
|
|
432
|
+
const containers = pod?.spec?.containers || []
|
|
433
|
+
const annotated = pod?.metadata?.annotations?.['kubectl.kubernetes.io/default-container']
|
|
434
|
+
if (annotated && containers.some((c: any) => c.name === annotated)) {
|
|
435
|
+
return annotated
|
|
436
|
+
}
|
|
437
|
+
return containers[0]?.name
|
|
438
|
+
}
|
|
439
|
+
|
|
421
440
|
export function getContainerSquareStates(pod: any): ContainerSquareState[] {
|
|
422
441
|
const result: ContainerSquareState[] = []
|
|
423
442
|
const initStatuses = pod.status?.initContainerStatuses || []
|
|
@@ -1610,6 +1629,10 @@ export function formatResources(resources: any): string {
|
|
|
1610
1629
|
if (resources.memory) {
|
|
1611
1630
|
parts.push(`Mem: ${formatMemoryString(resources.memory)}`)
|
|
1612
1631
|
}
|
|
1632
|
+
for (const [key, value] of Object.entries(resources)) {
|
|
1633
|
+
if (key === 'cpu' || key === 'memory') continue
|
|
1634
|
+
parts.push(`${key}: ${value}`)
|
|
1635
|
+
}
|
|
1613
1636
|
return parts.join(', ') || '-'
|
|
1614
1637
|
}
|
|
1615
1638
|
|
|
@@ -1631,8 +1654,12 @@ export function parseColumnFilters(filtersParam: string | null): Record<string,
|
|
|
1631
1654
|
for (const pair of filtersParam.split('|')) {
|
|
1632
1655
|
const colonIdx = pair.indexOf(':')
|
|
1633
1656
|
if (colonIdx > 0) {
|
|
1634
|
-
const
|
|
1657
|
+
const rawKey = pair.slice(0, colonIdx).trim()
|
|
1635
1658
|
const valStr = pair.slice(colonIdx + 1).trim()
|
|
1659
|
+
// Keys are URI-encoded so a custom-column key's own colon (e.g.
|
|
1660
|
+
// "label:tier") doesn't collide with the key:value delimiter.
|
|
1661
|
+
let key: string
|
|
1662
|
+
try { key = decodeURIComponent(rawKey) } catch { key = rawKey }
|
|
1636
1663
|
if (key && valStr) {
|
|
1637
1664
|
filters[key] = valStr.split(',').map(v => {
|
|
1638
1665
|
try { return decodeURIComponent(v.trim()) } catch { return v.trim() }
|
|
@@ -1643,12 +1670,13 @@ export function parseColumnFilters(filtersParam: string | null): Record<string,
|
|
|
1643
1670
|
return filters
|
|
1644
1671
|
}
|
|
1645
1672
|
|
|
1646
|
-
// Serialize column filters to URL param format
|
|
1647
|
-
//
|
|
1673
|
+
// Serialize column filters to URL param format. Keys and values are both
|
|
1674
|
+
// URI-encoded so a colon inside a custom-column key (e.g. "label:tier") or a
|
|
1675
|
+
// comma inside a value (e.g. "Ready,SchedulingDisabled") survives the round-trip.
|
|
1648
1676
|
export function serializeColumnFilters(filters: Record<string, string[]>): string {
|
|
1649
1677
|
const result = Object.entries(filters)
|
|
1650
1678
|
.filter(([, v]) => v.length > 0)
|
|
1651
|
-
.map(([k, vals]) => `${k}:${vals.map(v => encodeURIComponent(v)).join(',')}`)
|
|
1679
|
+
.map(([k, vals]) => `${encodeURIComponent(k)}:${vals.map(v => encodeURIComponent(v)).join(',')}`)
|
|
1652
1680
|
.join('|')
|
|
1653
1681
|
return result
|
|
1654
1682
|
}
|
|
@@ -1706,6 +1734,9 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
1706
1734
|
if (kindLower === 'scaledjobs') return getScaledJobStatus(resource).text
|
|
1707
1735
|
if (kindLower === 'policyreports' || kindLower === 'clusterpolicyreports') return _getPolicyReportStatus(resource).text
|
|
1708
1736
|
if (kindLower === 'kyvernopolicies' || kindLower === 'clusterpolicies') return _getKyvernoPolicyStatus(resource).text
|
|
1737
|
+
if (kindLower === 'nvidiaclusterpolicies') return _getNvidiaClusterPolicyStatus(resource).text
|
|
1738
|
+
if (kindLower === 'nvidiadrivers') return _getNvidiaDriverStatus(resource).text
|
|
1739
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimStatus(resource).text
|
|
1709
1740
|
if (kindLower === 'backups') return _getBackupStatus(resource).text
|
|
1710
1741
|
if (kindLower === 'restores') return _getRestoreStatus(resource).text
|
|
1711
1742
|
if (kindLower === 'schedules') return _getScheduleStatus(resource).text
|
|
@@ -1766,6 +1797,33 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
1766
1797
|
case 'provider':
|
|
1767
1798
|
if (kindLower === 'secretstores' || kindLower === 'clustersecretstores') return _getSecretStoreProviderType(resource)
|
|
1768
1799
|
return resource.spec?.provider || ''
|
|
1800
|
+
// DRA + NVIDIA columns. Unmatched kinds break to the generic fallback —
|
|
1801
|
+
// these keys are shared (e.g. 'components' is also a Trivy SBOM column).
|
|
1802
|
+
case 'deviceClass':
|
|
1803
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimDeviceClasses(resource).join(', ')
|
|
1804
|
+
if (kindLower === 'resourceclaimtemplates') return _getResourceClaimTemplateDeviceClasses(resource).join(', ')
|
|
1805
|
+
break
|
|
1806
|
+
case 'allocated':
|
|
1807
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimAllocation(resource).map(r => r.driver).join(', ')
|
|
1808
|
+
break
|
|
1809
|
+
case 'reservedFor':
|
|
1810
|
+
if (kindLower === 'resourceclaims') return _getResourceClaimReservedFor(resource).map(r => r.name).join(', ')
|
|
1811
|
+
break
|
|
1812
|
+
case 'pool':
|
|
1813
|
+
if (kindLower === 'resourceslices') return resource.spec?.pool?.name || ''
|
|
1814
|
+
break
|
|
1815
|
+
case 'devices':
|
|
1816
|
+
if (kindLower === 'resourceslices') return String((resource.spec?.devices || []).length)
|
|
1817
|
+
break
|
|
1818
|
+
case 'selectors':
|
|
1819
|
+
if (kindLower === 'deviceclasses') return String((resource.spec?.selectors || []).length)
|
|
1820
|
+
break
|
|
1821
|
+
case 'components':
|
|
1822
|
+
if (kindLower === 'nvidiaclusterpolicies') return _getNvidiaClusterPolicyEnabledComponents(resource).filter(c => c.enabled).map(c => c.label).join(', ')
|
|
1823
|
+
break
|
|
1824
|
+
case 'mig':
|
|
1825
|
+
if (kindLower === 'nvidiaclusterpolicies') return resource.spec?.mig?.strategy || ''
|
|
1826
|
+
break
|
|
1769
1827
|
}
|
|
1770
1828
|
|
|
1771
1829
|
// Fallback: try common paths
|
|
@@ -24,6 +24,7 @@ import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
|
24
24
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
25
25
|
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
26
26
|
import { formatKindName } from '../ui/drawer-components'
|
|
27
|
+
import { getDefaultContainerName } from '../resources/resource-utils'
|
|
27
28
|
|
|
28
29
|
// ============================================================================
|
|
29
30
|
// ACTIONS BAR - Interactive buttons that change based on resource kind
|
|
@@ -64,7 +65,7 @@ interface ResourceActionsBarProps {
|
|
|
64
65
|
renderPortForward?: (props: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => React.ReactNode
|
|
65
66
|
|
|
66
67
|
// Delete
|
|
67
|
-
onDelete?: (params: { kind: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
68
|
+
onDelete?: (params: { kind: string; group?: string; namespace: string; name: string; force: boolean }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
68
69
|
isDeleting?: boolean
|
|
69
70
|
cascadeDependents?: CascadeDependent[]
|
|
70
71
|
cascadeLoading?: boolean
|
|
@@ -165,7 +166,7 @@ export function ResourceActionsBar({
|
|
|
165
166
|
|
|
166
167
|
function handleDeleteConfirm(force: boolean) {
|
|
167
168
|
onDelete?.(
|
|
168
|
-
{ kind: resource.kind, namespace: resource.namespace, name: resource.name, force },
|
|
169
|
+
{ kind: resource.kind, group: resource.group, namespace: resource.namespace, name: resource.name, force },
|
|
169
170
|
{
|
|
170
171
|
onSuccess: () => {
|
|
171
172
|
setShowDeleteConfirm(false)
|
|
@@ -183,7 +184,7 @@ export function ResourceActionsBar({
|
|
|
183
184
|
onOpenTerminal?.({
|
|
184
185
|
namespace: resource.namespace,
|
|
185
186
|
podName: resource.name,
|
|
186
|
-
containerName: containers[0],
|
|
187
|
+
containerName: getDefaultContainerName(data) || containers[0],
|
|
187
188
|
containers,
|
|
188
189
|
})
|
|
189
190
|
}
|
|
@@ -195,7 +196,7 @@ export function ResourceActionsBar({
|
|
|
195
196
|
namespace: resource.namespace,
|
|
196
197
|
podName: resource.name,
|
|
197
198
|
containers,
|
|
198
|
-
containerName,
|
|
199
|
+
containerName: containerName || getDefaultContainerName(data),
|
|
199
200
|
})
|
|
200
201
|
}
|
|
201
202
|
}
|
|
@@ -43,3 +43,106 @@ describe('ResourceRendererDispatch', () => {
|
|
|
43
43
|
expect(html).toContain('none')
|
|
44
44
|
})
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
function renderKind(kind: string, data: any, namespace = ''): string {
|
|
48
|
+
return renderToString(
|
|
49
|
+
<ResourceRendererDispatch
|
|
50
|
+
resource={{ kind, namespace, name: data?.metadata?.name || 'x' }}
|
|
51
|
+
data={data}
|
|
52
|
+
onCopy={() => {}}
|
|
53
|
+
copied={null}
|
|
54
|
+
showCommonSections={false}
|
|
55
|
+
/>,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('ClusterPolicy kind collision (nvidia.com vs kyverno.io)', () => {
|
|
60
|
+
it('routes nvidia.com ClusterPolicy to the GPU Operator renderer', () => {
|
|
61
|
+
const html = renderKind('clusterpolicies', {
|
|
62
|
+
apiVersion: 'nvidia.com/v1',
|
|
63
|
+
kind: 'ClusterPolicy',
|
|
64
|
+
metadata: { name: 'cluster-policy' },
|
|
65
|
+
spec: { driver: { enabled: true }, devicePlugin: { enabled: true } },
|
|
66
|
+
status: { state: 'ready' },
|
|
67
|
+
})
|
|
68
|
+
expect(html).toContain('Operator Status')
|
|
69
|
+
expect(html).not.toContain('Rules')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('routes kyverno.io ClusterPolicy to the Kyverno renderer', () => {
|
|
73
|
+
const html = renderKind('clusterpolicies', {
|
|
74
|
+
apiVersion: 'kyverno.io/v1',
|
|
75
|
+
kind: 'ClusterPolicy',
|
|
76
|
+
metadata: { name: 'require-labels' },
|
|
77
|
+
spec: { rules: [{ name: 'check-labels' }] },
|
|
78
|
+
})
|
|
79
|
+
expect(html).not.toContain('Operator Status')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('DRA renderers dispatch', () => {
|
|
84
|
+
it('renders ResourceClaim with allocation sections (not GenericRenderer)', () => {
|
|
85
|
+
const html = renderKind('resourceclaims', {
|
|
86
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
87
|
+
kind: 'ResourceClaim',
|
|
88
|
+
metadata: { name: 'gpu-claim', namespace: 'ml' },
|
|
89
|
+
spec: { devices: { requests: [{ name: 'gpu', exactly: { deviceClassName: 'gpu.nvidia.com', count: 1 } }] } },
|
|
90
|
+
status: {
|
|
91
|
+
allocation: { devices: { results: [{ request: 'gpu', driver: 'gpu.nvidia.com', pool: 'node-1', device: 'gpu-0' }] } },
|
|
92
|
+
reservedFor: [{ resource: 'pods', name: 'train-1' }],
|
|
93
|
+
},
|
|
94
|
+
}, 'ml')
|
|
95
|
+
expect(html).toContain('Device Requests')
|
|
96
|
+
expect(html).toContain('gpu.nvidia.com')
|
|
97
|
+
expect(html).toContain('Reserved For')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not treat an empty allocation block as allocated', () => {
|
|
101
|
+
const html = renderKind('resourceclaims', {
|
|
102
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
103
|
+
kind: 'ResourceClaim',
|
|
104
|
+
metadata: { name: 'partial-claim', namespace: 'ml' },
|
|
105
|
+
spec: { devices: { requests: [{ name: 'gpu', exactly: { deviceClassName: 'gpu.example.com' } }] } },
|
|
106
|
+
status: { allocation: {} },
|
|
107
|
+
}, 'ml')
|
|
108
|
+
expect(html).toContain('Not allocated')
|
|
109
|
+
expect(html).not.toContain('Allocated but unreserved')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('reads v1beta1 request shape (deviceClassName at request level)', () => {
|
|
113
|
+
const html = renderKind('resourceclaims', {
|
|
114
|
+
apiVersion: 'resource.k8s.io/v1beta1',
|
|
115
|
+
kind: 'ResourceClaim',
|
|
116
|
+
metadata: { name: 'old-claim', namespace: 'ml' },
|
|
117
|
+
spec: { devices: { requests: [{ name: 'gpu', deviceClassName: 'gpu.example.com' }] } },
|
|
118
|
+
}, 'ml')
|
|
119
|
+
expect(html).toContain('gpu.example.com')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders ResourceSlice with device inventory', () => {
|
|
123
|
+
const html = renderKind('resourceslices', {
|
|
124
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
125
|
+
kind: 'ResourceSlice',
|
|
126
|
+
metadata: { name: 'node-1-gpus' },
|
|
127
|
+
spec: {
|
|
128
|
+
driver: 'gpu.nvidia.com',
|
|
129
|
+
pool: { name: 'node-1' },
|
|
130
|
+
nodeName: 'node-1',
|
|
131
|
+
devices: [{ name: 'gpu-0', attributes: { productName: { string: 'H100' } } }],
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
expect(html).toContain('Slice Info')
|
|
135
|
+
expect(html).toContain('H100')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('renders DeviceClass selectors', () => {
|
|
139
|
+
const html = renderKind('deviceclasses', {
|
|
140
|
+
apiVersion: 'resource.k8s.io/v1',
|
|
141
|
+
kind: 'DeviceClass',
|
|
142
|
+
metadata: { name: 'gpu.nvidia.com' },
|
|
143
|
+
spec: { selectors: [{ cel: { expression: "device.driver == 'gpu.nvidia.com'" } }] },
|
|
144
|
+
})
|
|
145
|
+
expect(html).toContain('Selectors (1)')
|
|
146
|
+
expect(html).toContain('device.driver')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -50,6 +50,8 @@ import { getNodePoolStatus, getNodeClaimStatus, getEC2NodeClassStatus } from '..
|
|
|
50
50
|
import { getScaledObjectStatus, getScaledJobStatus } from '../resources/resource-utils-keda'
|
|
51
51
|
import { getServiceMonitorStatus, getPrometheusRuleStatus, getPodMonitorStatus } from '../resources/resource-utils-prometheus'
|
|
52
52
|
import { getPolicyReportStatus, getKyvernoPolicyStatus } from '../resources/resource-utils-kyverno'
|
|
53
|
+
import { getResourceClaimStatus, getResourceClaimTemplateStatus, getDeviceClassStatus, getResourceSliceStatus } from '../resources/resource-utils-dra'
|
|
54
|
+
import { getNvidiaClusterPolicyStatus, getNvidiaDriverStatus } from '../resources/resource-utils-nvidia'
|
|
53
55
|
import { getBackupStatus, getRestoreStatus, getScheduleStatus, getBSLStatus } from '../resources/resource-utils-velero'
|
|
54
56
|
import {
|
|
55
57
|
getVirtualServiceStatus,
|
|
@@ -208,6 +210,12 @@ import {
|
|
|
208
210
|
CompositionRenderer,
|
|
209
211
|
CompositionRevisionRenderer,
|
|
210
212
|
XRDRenderer,
|
|
213
|
+
ResourceClaimRenderer,
|
|
214
|
+
ResourceClaimTemplateRenderer,
|
|
215
|
+
DeviceClassRenderer,
|
|
216
|
+
ResourceSliceRenderer,
|
|
217
|
+
NvidiaClusterPolicyRenderer,
|
|
218
|
+
NvidiaDriverRenderer,
|
|
211
219
|
} from '../resources/renderers'
|
|
212
220
|
import type { ComposedRefStatus } from '../resources/renderers/CompositeRenderer'
|
|
213
221
|
import {
|
|
@@ -318,6 +326,8 @@ const KNOWN_KINDS = new Set([
|
|
|
318
326
|
'triggerauthentications', 'clustertriggerauthentications',
|
|
319
327
|
'servicemonitors', 'prometheusrules', 'podmonitors',
|
|
320
328
|
'policyreports', 'clusterpolicyreports', 'kyvernopolicies', 'clusterpolicies',
|
|
329
|
+
'resourceclaims', 'resourceclaimtemplates', 'deviceclasses', 'resourceslices',
|
|
330
|
+
'nvidiadrivers',
|
|
321
331
|
'vulnerabilityreports', 'configauditreports', 'exposedsecretreports',
|
|
322
332
|
'rbacassessmentreports', 'clusterrbacassessmentreports',
|
|
323
333
|
'clustercompliancereports', 'sbomreports', 'clustersbomreports',
|
|
@@ -557,7 +567,14 @@ export function ResourceRendererDispatch({
|
|
|
557
567
|
{kind === 'prometheusrules' && <PrometheusRuleRenderer data={data} />}
|
|
558
568
|
{kind === 'podmonitors' && <PodMonitorRenderer data={data} />}
|
|
559
569
|
{(kind === 'policyreports' || kind === 'clusterpolicyreports') && <PolicyReportRenderer data={data} />}
|
|
560
|
-
{(kind === 'kyvernopolicies' || kind === 'clusterpolicies') && <KyvernoPolicyRenderer data={data} />}
|
|
570
|
+
{(kind === 'kyvernopolicies' || (kind === 'clusterpolicies' && !data?.apiVersion?.startsWith('nvidia.com/'))) && <KyvernoPolicyRenderer data={data} />}
|
|
571
|
+
{(kind === 'clusterpolicies' && data?.apiVersion?.startsWith('nvidia.com/')) && <NvidiaClusterPolicyRenderer data={data} />}
|
|
572
|
+
{kind === 'nvidiadrivers' && <NvidiaDriverRenderer data={data} />}
|
|
573
|
+
{/* DRA (resource.k8s.io) */}
|
|
574
|
+
{kind === 'resourceclaims' && <ResourceClaimRenderer data={data} onNavigate={onNavigate} />}
|
|
575
|
+
{kind === 'resourceclaimtemplates' && <ResourceClaimTemplateRenderer data={data} />}
|
|
576
|
+
{kind === 'deviceclasses' && <DeviceClassRenderer data={data} />}
|
|
577
|
+
{kind === 'resourceslices' && <ResourceSliceRenderer data={data} onNavigate={onNavigate} />}
|
|
561
578
|
{kind === 'backups' && data.apiVersion?.includes('cnpg.io') && <CNPGBackupRenderer data={data} onNavigate={onNavigate} />}
|
|
562
579
|
{kind === 'backups' && !data.apiVersion?.includes('cnpg.io') && <VeleroBackupRenderer data={data} />}
|
|
563
580
|
{kind === 'restores' && <VeleroRestoreRenderer data={data} />}
|
|
@@ -760,7 +777,15 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
760
777
|
if (k === 'clustercompliancereports') return getClusterComplianceReportStatus(data)
|
|
761
778
|
if (k === 'sbomreports' || k === 'clustersbomreports') return getSbomReportStatus(data)
|
|
762
779
|
if (k === 'policyreports' || k === 'clusterpolicyreports') return getPolicyReportStatus(data)
|
|
763
|
-
if (k === 'kyvernopolicies' || k === 'clusterpolicies')
|
|
780
|
+
if (k === 'kyvernopolicies' || k === 'clusterpolicies') {
|
|
781
|
+
if (data?.apiVersion?.startsWith('nvidia.com/')) return getNvidiaClusterPolicyStatus(data)
|
|
782
|
+
return getKyvernoPolicyStatus(data)
|
|
783
|
+
}
|
|
784
|
+
if (k === 'nvidiadrivers') return getNvidiaDriverStatus(data)
|
|
785
|
+
if (k === 'resourceclaims') return getResourceClaimStatus(data)
|
|
786
|
+
if (k === 'resourceclaimtemplates') return getResourceClaimTemplateStatus(data)
|
|
787
|
+
if (k === 'deviceclasses') return getDeviceClassStatus(data)
|
|
788
|
+
if (k === 'resourceslices') return getResourceSliceStatus(data)
|
|
764
789
|
if (k === 'backups') {
|
|
765
790
|
if (data.apiVersion?.includes('cnpg.io')) return getCNPGBackupStatus(data)
|
|
766
791
|
return getBackupStatus(data)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
-
import { formatAge, formatDuration } from '../resources/resource-utils'
|
|
4
|
+
import { formatAge, formatDuration, formatResources } from '../resources/resource-utils'
|
|
5
|
+
import { getEffectiveResources } from '../../utils/extended-resources'
|
|
5
6
|
import { Tooltip } from './Tooltip'
|
|
6
7
|
import { getKindColorClass } from '../ui/Badge'
|
|
7
8
|
|
|
@@ -489,6 +490,18 @@ export function MetadataSection({ data }: { data: any }) {
|
|
|
489
490
|
)
|
|
490
491
|
}
|
|
491
492
|
|
|
493
|
+
// Templates never get apiserver request-defaulting, so render the effective
|
|
494
|
+
// view (requests, falling back to limits) — limits-only GPU specs included.
|
|
495
|
+
function ContainerResourcesLine({ resources }: { resources: any }) {
|
|
496
|
+
const effective = getEffectiveResources(resources)
|
|
497
|
+
if (Object.keys(effective).length === 0) return null
|
|
498
|
+
return (
|
|
499
|
+
<div className="text-xs text-theme-text-tertiary mt-1" title="Effective requests (requests, falling back to limits)">
|
|
500
|
+
Resources: {formatResources(effective)}
|
|
501
|
+
</div>
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
|
|
492
505
|
export function PodTemplateSection({ template }: { template: any }) {
|
|
493
506
|
if (!template) return null
|
|
494
507
|
const initContainers = template.spec?.initContainers || []
|
|
@@ -508,6 +521,7 @@ export function PodTemplateSection({ template }: { template: any }) {
|
|
|
508
521
|
$ {[...(c.command || []), ...(c.args || [])].join(' ')}
|
|
509
522
|
</div>
|
|
510
523
|
)}
|
|
524
|
+
<ContainerResourcesLine resources={c.resources} />
|
|
511
525
|
</div>
|
|
512
526
|
))}
|
|
513
527
|
<div className="text-xs text-theme-text-tertiary font-medium uppercase tracking-wide mt-3">Containers</div>
|
|
@@ -522,6 +536,7 @@ export function PodTemplateSection({ template }: { template: any }) {
|
|
|
522
536
|
Ports: {c.ports.map((p: any) => `${p.name ? `${p.name}: ` : ''}${p.containerPort}/${p.protocol || 'TCP'}`).join(', ')}
|
|
523
537
|
</div>
|
|
524
538
|
)}
|
|
539
|
+
<ContainerResourcesLine resources={c.resources} />
|
|
525
540
|
</div>
|
|
526
541
|
))}
|
|
527
542
|
</div>
|
|
@@ -664,6 +679,9 @@ export function formatKindName(kind: string): string {
|
|
|
664
679
|
horizontalpodautoscalers: 'HPA', nodes: 'Node', namespaces: 'Namespace',
|
|
665
680
|
persistentvolumeclaims: 'PVC', persistentvolumes: 'PV',
|
|
666
681
|
httpproxies: 'HTTPProxy',
|
|
682
|
+
resourceclaims: 'ResourceClaim', resourceclaimtemplates: 'ResourceClaimTemplate',
|
|
683
|
+
deviceclasses: 'DeviceClass', resourceslices: 'ResourceSlice',
|
|
684
|
+
clusterpolicies: 'ClusterPolicy', nvidiadrivers: 'NVIDIADriver',
|
|
667
685
|
}
|
|
668
686
|
if (names[k]) return names[k]
|
|
669
687
|
|
|
@@ -726,6 +744,7 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
726
744
|
(relationships.scalers && relationships.scalers.length > 0) ||
|
|
727
745
|
(relationships.pdbs && relationships.pdbs.length > 0) ||
|
|
728
746
|
(relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
|
|
747
|
+
(relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
|
|
729
748
|
relationships.scaleTarget
|
|
730
749
|
|
|
731
750
|
if (!hasRelationships) return null
|
|
@@ -772,6 +791,9 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
772
791
|
{relationships.networkPolicies && relationships.networkPolicies.length > 0 && (
|
|
773
792
|
<RelationshipGroup label="Network Policies" refs={dedupeRefs(relationships.networkPolicies)} onNavigate={onNavigate} />
|
|
774
793
|
)}
|
|
794
|
+
{relationships.resourceClaims && relationships.resourceClaims.length > 0 && (
|
|
795
|
+
<RelationshipGroup label="Resource Claims" refs={dedupeRefs(relationships.resourceClaims)} onNavigate={onNavigate} />
|
|
796
|
+
)}
|
|
775
797
|
{relationships.scaleTarget && (
|
|
776
798
|
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
777
799
|
)}
|
package/src/types/core.ts
CHANGED
|
@@ -441,6 +441,7 @@ export interface Relationships {
|
|
|
441
441
|
pods?: ResourceRef[]
|
|
442
442
|
serviceAccount?: ResourceRef // For Pods: derived from pod.spec.serviceAccountName
|
|
443
443
|
node?: ResourceRef // For scheduled Pods: derived from pod.spec.nodeName
|
|
444
|
+
resourceClaims?: ResourceRef[] // For Pods: DRA ResourceClaims (direct + template-generated)
|
|
444
445
|
}
|
|
445
446
|
|
|
446
447
|
// Parsed X.509 certificate metadata (from backend cert parsing)
|
|
@@ -170,6 +170,27 @@ export function formatGroupName(group: string): string {
|
|
|
170
170
|
'sparkoperator.k8s.io': 'Spark',
|
|
171
171
|
'kubeflow.org': 'Kubeflow',
|
|
172
172
|
'snapshot.storage.k8s.io': 'Snapshots',
|
|
173
|
+
'karpenter.sh': 'Karpenter',
|
|
174
|
+
'karpenter.k8s.aws': 'Karpenter',
|
|
175
|
+
'karpenter.azure.com': 'Karpenter',
|
|
176
|
+
'karpenter.k8s.gcp': 'Karpenter',
|
|
177
|
+
'resource.k8s.io': 'Dynamic Resource Allocation',
|
|
178
|
+
'kueue.x-k8s.io': 'Kueue',
|
|
179
|
+
'autoscaling.x-k8s.io': 'Cluster Autoscaler',
|
|
180
|
+
'serving.kserve.io': 'KServe',
|
|
181
|
+
'ray.io': 'KubeRay',
|
|
182
|
+
'leaderworkerset.x-k8s.io': 'LeaderWorkerSet',
|
|
183
|
+
'jobset.x-k8s.io': 'JobSet',
|
|
184
|
+
'inference.networking.k8s.io': 'Inference Gateway',
|
|
185
|
+
'inference.networking.x-k8s.io': 'Inference Gateway',
|
|
186
|
+
'nvidia.com': 'NVIDIA GPU Operator',
|
|
187
|
+
'scheduling.run.ai': 'KAI Scheduler',
|
|
188
|
+
'kai.scheduler': 'KAI Scheduler',
|
|
189
|
+
'kaito.sh': 'KAITO',
|
|
190
|
+
'batch.volcano.sh': 'Volcano',
|
|
191
|
+
'scheduling.volcano.sh': 'Volcano',
|
|
192
|
+
'flow.volcano.sh': 'Volcano',
|
|
193
|
+
'bus.volcano.sh': 'Volcano',
|
|
173
194
|
}
|
|
174
195
|
if (knownGroups[group]) return knownGroups[group]
|
|
175
196
|
// Suffix rules — for unbounded provider group sets (Crossplane providers ship
|