/** * NodeDetailSection — injected into Headlamp's native Node detail page. * * Shows Intel GPU resources available on the node (capacity, allocatable), * GPU type, and pods currently using GPU resources on this node. * Returns null for non-GPU nodes. */ import { NameValueTable, SectionBox, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React from 'react'; import { useIntelGpuContext } from '../api/IntelGpuDataContext'; import { formatGpuResourceName, formatGpuType, getGpuResources, getNodeGpuType, INTEL_GPU_RESOURCE, INTEL_GPU_XE_RESOURCE, isIntelGpuNode, } from '../api/k8s'; interface NodeDetailSectionProps { resource: { kind?: string; metadata?: { name?: string; labels?: Record }; jsonData?: unknown; // Headlamp KubeObject may expose status directly or via jsonData status?: unknown; }; } export default function NodeDetailSection({ resource }: NodeDetailSectionProps) { const { gpuPods, loading } = useIntelGpuContext(); // Extract the raw Kubernetes JSON — Headlamp KubeObject wraps it in jsonData const rawNode = resource.jsonData && typeof resource.jsonData === 'object' ? resource.jsonData : resource; // Only render for Node resources that have Intel GPU if (!isIntelGpuNode(rawNode)) return null; const node = rawNode as Parameters[0] & { status?: { capacity?: Record; allocatable?: Record; nodeInfo?: { kernelVersion?: string; osImage?: string }; }; metadata: { name: string; labels?: Record }; }; const nodeName = node.metadata.name; const capacity = getGpuResources(node.status?.capacity); const allocatable = getGpuResources(node.status?.allocatable); const gpuType = getNodeGpuType(node); // Find GPU pods scheduled on this node const podsOnNode = loading ? [] : gpuPods.filter(p => p.spec?.nodeName === nodeName); if (Object.keys(capacity).length === 0 && Object.keys(allocatable).length === 0) { return null; } // GPU utilization: count GPU units used by running pods let gpuInUse = 0; let gpuAllocatable = 0; for (const [key, val] of Object.entries(allocatable)) { if (key === INTEL_GPU_RESOURCE || key === INTEL_GPU_XE_RESOURCE) { gpuAllocatable += parseInt(val, 10) || 0; } } for (const pod of podsOnNode.filter(p => p.status?.phase === 'Running')) { const reqs = pod.spec?.containers?.flatMap(c => Object.entries(c.resources?.requests ?? {}).filter( ([k]) => k === INTEL_GPU_RESOURCE || k === INTEL_GPU_XE_RESOURCE ) ) ?? []; for (const [, val] of reqs) { gpuInUse += parseInt(val, 10) || 0; } } const utilizationPct = gpuAllocatable > 0 ? Math.round((gpuInUse / gpuAllocatable) * 100) : 0; const utilizationStatus: 'success' | 'warning' | 'error' = utilizationPct >= 90 ? 'error' : utilizationPct >= 70 ? 'warning' : 'success'; return ( ({ name: `${formatGpuResourceName(key)} (capacity)`, value: val, })), // Allocatable rows ...Object.entries(allocatable).map(([key, val]) => ({ name: `${formatGpuResourceName(key)} (allocatable)`, value: val, })), // Utilization ...(gpuAllocatable > 0 ? [ { name: 'GPU Utilization', value: ( {`${gpuInUse}/${gpuAllocatable} (${utilizationPct}%)`} ), }, ] : []), // Workload pods { name: 'GPU Workload Pods', value: podsOnNode.length > 0 ? podsOnNode.map(p => p.metadata.name).join(', ') : loading ? 'Loading…' : 'None', }, ]} /> ); }