feat: initial release of headlamp-intel-gpu-plugin v0.1.0
Adds a Headlamp plugin for Intel GPU device plugin visibility: - Dedicated sidebar section: Overview, Device Plugins, GPU Nodes, GPU Pods - Native Node detail page injection: GPU capacity, allocatable, utilization, active pods - Native Pod detail page injection: per-container GPU resource requests/limits - Native Nodes table: GPU Type and GPU Devices columns - App bar health badge (hidden when plugin not installed) - GpuDevicePlugin CRD monitoring (deviceplugin.intel.com/v1) with graceful degradation when CRD is not present - Supports discrete (i915), Xe, and integrated GPU nodes via node labels - 48 unit tests, TypeScript clean, 28 kB production bundle Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* DevicePluginsPage — lists all GpuDevicePlugin CRD instances.
|
||||
*
|
||||
* Shows configuration details for each Intel GPU device plugin deployment,
|
||||
* including spec and status information.
|
||||
*/
|
||||
|
||||
import {
|
||||
Loader,
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
SectionHeader,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { formatAge, isPodReady, pluginStatusText, pluginStatusToStatus } from '../api/k8s';
|
||||
|
||||
export default function DevicePluginsPage() {
|
||||
const { devicePlugins, pluginPods, crdAvailable, loading, error, refresh } =
|
||||
useIntelGpuContext();
|
||||
|
||||
if (loading) {
|
||||
return <Loader title="Loading device plugin data..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<SectionHeader title="Intel GPU — Device Plugins" />
|
||||
<button
|
||||
onClick={refresh}
|
||||
aria-label="Refresh device plugin data"
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--mui-palette-primary-main, #0071c5)',
|
||||
border: '1px solid var(--mui-palette-primary-main, #0071c5)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<SectionBox title="Error">
|
||||
<NameValueTable
|
||||
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{!crdAvailable && (
|
||||
<SectionBox title="CRD Not Available">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: (
|
||||
<StatusLabel status="warning">
|
||||
GpuDevicePlugin CRD (deviceplugin.intel.com/v1) is not installed
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value:
|
||||
'Install the Intel Device Plugins Operator to manage GpuDevicePlugin resources. ' +
|
||||
'Plugin daemon pods are shown below if detected.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{/* GpuDevicePlugin CRD instances */}
|
||||
{crdAvailable && devicePlugins.length === 0 && (
|
||||
<SectionBox title="No Device Plugins">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: (
|
||||
<StatusLabel status="warning">
|
||||
No GpuDevicePlugin resources found on this cluster
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value:
|
||||
'kubectl apply -f gpudeviceplugin.yaml (see Intel documentation for configuration)',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{devicePlugins.map(plugin => (
|
||||
<SectionBox key={plugin.metadata.uid ?? plugin.metadata.name} title={`GpuDevicePlugin: ${plugin.metadata.name}`}>
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: (
|
||||
<StatusLabel status={pluginStatusToStatus(plugin)}>
|
||||
{pluginStatusText(plugin)}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Image',
|
||||
value: plugin.spec.image ?? '—',
|
||||
},
|
||||
{
|
||||
name: 'Shared Devices/Node',
|
||||
value: String(plugin.spec.sharedDevNum ?? 1),
|
||||
},
|
||||
{
|
||||
name: 'Allocation Policy',
|
||||
value: plugin.spec.preferredAllocationPolicy ?? 'default',
|
||||
},
|
||||
{
|
||||
name: 'Monitoring',
|
||||
value: plugin.spec.enableMonitoring ? (
|
||||
<StatusLabel status="success">Enabled</StatusLabel>
|
||||
) : (
|
||||
<StatusLabel status="warning">Disabled</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Resource Manager',
|
||||
value: plugin.spec.resourceManager ? 'Enabled' : 'Disabled',
|
||||
},
|
||||
{
|
||||
name: 'Desired Nodes',
|
||||
value: String(plugin.status?.desiredNumberScheduled ?? '—'),
|
||||
},
|
||||
{
|
||||
name: 'Ready Nodes',
|
||||
value: String(plugin.status?.numberReady ?? '—'),
|
||||
},
|
||||
...(plugin.status?.numberUnavailable
|
||||
? [{
|
||||
name: 'Unavailable Nodes',
|
||||
value: (
|
||||
<StatusLabel status="error">
|
||||
{plugin.status.numberUnavailable}
|
||||
</StatusLabel>
|
||||
),
|
||||
}]
|
||||
: []),
|
||||
{
|
||||
name: 'Node Selector',
|
||||
value: plugin.spec.nodeSelector
|
||||
? Object.entries(plugin.spec.nodeSelector)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(', ')
|
||||
: '—',
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: formatAge(plugin.metadata.creationTimestamp),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
))}
|
||||
|
||||
{/* Plugin daemon pods */}
|
||||
{pluginPods.length > 0 && (
|
||||
<SectionBox title="Plugin Daemon Pods">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
||||
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
|
||||
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
|
||||
{
|
||||
label: 'Ready',
|
||||
getter: (p) => (
|
||||
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
|
||||
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Restarts',
|
||||
getter: (p) => {
|
||||
const restarts = p.status?.containerStatuses?.reduce(
|
||||
(sum, c) => sum + c.restartCount, 0
|
||||
) ?? 0;
|
||||
return restarts > 0 ? (
|
||||
<StatusLabel status="warning">{restarts}</StatusLabel>
|
||||
) : (
|
||||
String(restarts)
|
||||
);
|
||||
},
|
||||
},
|
||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
||||
]}
|
||||
data={pluginPods}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user