@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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/components/dock/TerminalTab.tsx +4 -2
  3. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
  4. package/src/components/issues/IssuesView.tsx +64 -17
  5. package/src/components/issues/index.ts +1 -1
  6. package/src/components/issues/issues.test.ts +4 -4
  7. package/src/components/issues/severity.ts +5 -0
  8. package/src/components/issues/types.ts +43 -0
  9. package/src/components/logs/LogCore.tsx +13 -2
  10. package/src/components/logs/LogToolbarSelects.tsx +6 -2
  11. package/src/components/logs/LogsViewer.tsx +66 -10
  12. package/src/components/logs/WorkloadLogsViewer.tsx +60 -8
  13. package/src/components/logs/useLogStream.ts +41 -3
  14. package/src/components/resources/ResourcesView.tsx +550 -52
  15. package/src/components/resources/column-filter-serialization.test.ts +26 -0
  16. package/src/components/resources/get-default-container-name.test.ts +31 -0
  17. package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
  18. package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
  19. package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
  20. package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
  21. package/src/components/resources/renderers/PodRenderer.tsx +2 -2
  22. package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
  23. package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
  24. package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
  25. package/src/components/resources/renderers/dra-cells.tsx +80 -0
  26. package/src/components/resources/renderers/index.ts +8 -0
  27. package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
  28. package/src/components/resources/resource-utils-dra.ts +90 -0
  29. package/src/components/resources/resource-utils-nvidia.ts +63 -0
  30. package/src/components/resources/resource-utils.ts +62 -4
  31. package/src/components/shared/ResourceActionsBar.tsx +5 -4
  32. package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
  33. package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
  34. package/src/components/ui/ConfirmDialog.tsx +1 -1
  35. package/src/components/ui/drawer-components.tsx +23 -1
  36. package/src/types/core.ts +1 -0
  37. package/src/utils/api-resources.ts +21 -0
  38. package/src/utils/custom-columns.test.ts +111 -0
  39. package/src/utils/custom-columns.ts +49 -0
  40. package/src/utils/extended-resources.test.ts +152 -0
  41. package/src/utils/extended-resources.ts +121 -0
  42. 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 key = pair.slice(0, colonIdx).trim()
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
- // Values are URI-encoded so commas inside values (e.g. "Ready,SchedulingDisabled") survive the round-trip.
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') return getKyvernoPolicyStatus(data)
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)
@@ -74,7 +74,7 @@ export function ConfirmDialog({
74
74
 
75
75
  {/* Custom content */}
76
76
  {children && (
77
- <div className="px-4 pt-4">
77
+ <div className="px-4 py-4">
78
78
  {children}
79
79
  </div>
80
80
  )}
@@ -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