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:
2026-02-18 17:58:49 -05:00
commit 41bf2aead4
18 changed files with 21053 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
/**
* IntelGpuDataContext — shared data provider for Intel GPU device plugin resources.
*
* Wraps K8s hook calls and ApiProxy requests, providing filtered Intel GPU
* resources to all child pages through React context, avoiding prop drilling
* and duplicate API calls.
*/
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
filterGpuRequestingPods,
filterIntelGpuNodes,
filterIntelGpuPluginPods,
GpuDevicePlugin,
INTEL_DEVICE_PLUGIN_API_GROUP,
INTEL_DEVICE_PLUGIN_API_VERSION,
IntelGpuNode,
IntelGpuPod,
isGpuDevicePlugin,
isKubeList,
} from './k8s';
// ---------------------------------------------------------------------------
// Context shape
// ---------------------------------------------------------------------------
export interface IntelGpuContextValue {
/** GpuDevicePlugin CRD instances — one per GPU type/config */
devicePlugins: GpuDevicePlugin[];
/** True if at least one GpuDevicePlugin CR exists */
pluginInstalled: boolean;
/** Nodes that have Intel GPU resources or labels */
gpuNodes: IntelGpuNode[];
/** Pods requesting Intel GPU resources */
gpuPods: IntelGpuPod[];
/** Intel GPU device plugin daemon pods */
pluginPods: IntelGpuPod[];
/** True if the GpuDevicePlugin CRD is available on the cluster */
crdAvailable: boolean;
/** Loading / error state */
loading: boolean;
error: string | null;
/** Manual refresh trigger */
refresh: () => void;
}
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
const IntelGpuContext = createContext<IntelGpuContextValue | null>(null);
export function useIntelGpuContext(): IntelGpuContextValue {
const ctx = useContext(IntelGpuContext);
if (!ctx) {
throw new Error('useIntelGpuContext must be used within an IntelGpuDataProvider');
}
return ctx;
}
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
export function IntelGpuDataProvider({ children }: { children: React.ReactNode }) {
// K8s resource hooks — headlamp re-fetches on cluster context changes
const [allNodes, nodeError] = K8s.ResourceClasses.Node.useList();
const [allPods, podError] = K8s.ResourceClasses.Pod.useList({ namespace: '' });
// Async state for CRD resources
const [devicePlugins, setDevicePlugins] = useState<GpuDevicePlugin[]>([]);
const [pluginPods, setPluginPods] = useState<IntelGpuPod[]>([]);
const [crdAvailable, setCrdAvailable] = useState(false);
const [asyncLoading, setAsyncLoading] = useState(true);
const [asyncError, setAsyncError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const refresh = useCallback(() => {
setRefreshKey(k => k + 1);
}, []);
useEffect(() => {
let cancelled = false;
async function fetchAsync() {
setAsyncLoading(true);
setAsyncError(null);
try {
// GpuDevicePlugin CRDs — graceful degradation if CRD not installed
try {
const pluginList = await ApiProxy.request(
`/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins`
);
if (!cancelled && isKubeList(pluginList)) {
setCrdAvailable(true);
setDevicePlugins(pluginList.items.filter(isGpuDevicePlugin));
}
} catch {
if (!cancelled) {
setCrdAvailable(false);
setDevicePlugins([]);
}
}
// Intel GPU plugin DaemonSet pods — look across all namespaces
// The device plugin is commonly deployed in kube-system but may vary
const pluginPodSelectors = [
// Intel device plugins operator deployment
`/api/v1/pods?labelSelector=${encodeURIComponent('app=intel-gpu-plugin')}`,
// Alternative: by component label
`/api/v1/pods?labelSelector=${encodeURIComponent('app.kubernetes.io/name=intel-gpu-plugin')}`,
// Intel device plugins from inteldeviceplugins-system namespace
`/api/v1/namespaces/inteldeviceplugins-system/pods`,
];
const foundPluginPods: IntelGpuPod[] = [];
for (const url of pluginPodSelectors) {
try {
const list = await ApiProxy.request(url);
if (!cancelled && isKubeList(list)) {
const gpuPluinPods = filterIntelGpuPluginPods(list.items);
foundPluginPods.push(...gpuPluinPods);
}
} catch {
// Silently ignore — some selectors may not match
}
}
// Deduplicate by pod UID
const seen = new Set<string>();
const uniquePluginPods = foundPluginPods.filter(p => {
const uid = p.metadata.uid;
if (!uid || seen.has(uid)) return false;
seen.add(uid);
return true;
});
if (!cancelled) setPluginPods(uniquePluginPods);
} catch (err: unknown) {
if (!cancelled) {
setAsyncError(err instanceof Error ? err.message : String(err));
}
} finally {
if (!cancelled) setAsyncLoading(false);
}
}
void fetchAsync();
return () => { cancelled = true; };
}, [refreshKey]);
// ---------------------------------------------------------------------------
// Derived / filtered values — memoized to avoid recomputation on every render
//
// Headlamp useList() returns KubeObject class instances that store raw
// Kubernetes JSON under `.jsonData`. Extract jsonData so our plain-object
// type helpers work correctly.
// ---------------------------------------------------------------------------
const extractJsonData = (items: unknown[]): unknown[] =>
items.map(item =>
item && typeof item === 'object' && 'jsonData' in item
? (item as { jsonData: unknown }).jsonData
: item
);
const gpuNodes = useMemo(() => {
if (!allNodes) return [];
return filterIntelGpuNodes(extractJsonData(allNodes as unknown[]));
}, [allNodes]);
const gpuPods = useMemo(() => {
if (!allPods) return [];
return filterGpuRequestingPods(extractJsonData(allPods as unknown[]));
}, [allPods]);
// ---------------------------------------------------------------------------
// Combined loading / error state
// ---------------------------------------------------------------------------
const loading = asyncLoading || !allNodes || !allPods;
const errors: string[] = [];
if (nodeError) errors.push(String(nodeError));
if (podError) errors.push(String(podError));
if (asyncError) errors.push(asyncError);
const error = errors.length > 0 ? errors.join('; ') : null;
const pluginInstalled = devicePlugins.length > 0 || pluginPods.length > 0;
// ---------------------------------------------------------------------------
// Memoized context value
// ---------------------------------------------------------------------------
const value = useMemo<IntelGpuContextValue>(
() => ({
devicePlugins,
pluginInstalled,
gpuNodes,
gpuPods,
pluginPods,
crdAvailable,
loading,
error,
refresh,
}),
[
devicePlugins,
pluginInstalled,
gpuNodes,
gpuPods,
pluginPods,
crdAvailable,
loading,
error,
refresh,
]
);
return <IntelGpuContext.Provider value={value}>{children}</IntelGpuContext.Provider>;
}