Compare commits

..

26 Commits

Author SHA1 Message Date
github-actions[bot] 60d76f1cb2 release: v0.4.3 2026-03-19 21:39:48 +00:00
privilegedescalation-paperclip[bot] 0d72d07048 fix: add pull-requests write permission to release workflow (#13)
The reusable release workflow declares pull-requests:write but the
caller didn't grant it, causing startup_failure on GitHub Actions.

Co-authored-by: Hugh Hackman [bot] <hugh-hackman[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 21:33:16 +00:00
gandalf-the-greybeard[bot] daad91880c fix: add missing devDependencies for CI (#12)
The package.json only listed @kinvolk/headlamp-plugin as a devDependency,
but CI runs tsc, eslint, prettier, and vitest which all require additional
packages. Add the same devDependencies used by the reference kube-vip plugin
and regenerate the lock file.

Also adds peerDependencies for react/react-dom to match the reference plugin
conventions.

Co-authored-by: Gandalf the Greybeard <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-03-18 23:30:28 +00:00
null-pointer-nancy[bot] b9137958f0 Merge pull request #11 from privilegedescalation/fix/dep-security-overrides-tar-undici
fix: add npm overrides for tar and undici security advisories
2026-03-18 23:14:06 +00:00
Hugh Hackman 37a2232178 fix: regenerate package-lock.json for undici override
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 23:08:00 +00:00
Hugh Hackman 56eb0761dd fix: add npm overrides for tar and undici security advisories
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-18 22:55:50 +00:00
null-pointer-nancy[bot] 18c6a03c0c Merge pull request #9 from privilegedescalation/docs/remove-manual-install
docs: remove manual install sections from README
2026-03-17 12:19:29 +00:00
Gandalf the Greybeard cbd86f696d docs: remove manual install sections from README
ArtifactHub plugin installer is the only supported installation method.
Remove manual tarball, sidecar, and build-from-source install options
to align documentation with company policy.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 12:15:45 +00:00
null-pointer-nancy[bot] 510affbe1a ci: retrigger after shared workflow fix (#8)
CI retrigger after shared workflow fix (.github PR#14)
2026-03-15 17:54:40 +00:00
Chris Farhood fcb2e5f9fd Merge pull request #7 from privilegedescalation/policy/artifacthub-only
policy: add ArtifactHub-only installation requirement
2026-03-15 12:43:25 -04:00
null-pointer-nancy[bot] a34802b477 policy: add ArtifactHub-only installation policy
Per CEO directive, ArtifactHub via the Headlamp plugin installer is the
only approved installation method. No exceptions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 16:36:41 +00:00
gandalf-the-greybeard[bot] e5e681b415 fix: rename plugin from headlamp-intel-gpu to intel-gpu (#6)
Aligns naming convention across all plugins. Renames package, sidebar entries, routes, and documentation references.
2026-03-10 23:49:08 +00:00
github-actions[bot] db896a8f88 release: v0.4.2 2026-03-09 03:11:10 +00:00
DevContainer User a16df9baf7 fix: add archive checksum to ArtifactHub metadata
Empty checksum causes headlamp plugin manager to reject the plugin
with "Invalid plugin metadata".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 02:55:56 +00:00
Chris Farhood 865168285e Merge pull request #5 from privilegedescalation/feat/add-upstream-appversion-tracking
feat: auto-track upstream appVersion in releases
2026-03-08 11:41:43 -04:00
Chris Farhood 84af42147f Merge pull request #4 from privilegedescalation/rename-plugin-headlamp-intel-gpu
Rename plugin from intel-gpu to headlamp-intel-gpu
2026-03-08 11:41:10 -04:00
Hugh Hackman b0de53577a feat: add upstream appVersion tracking to release workflow
Configures the reusable release workflow to fetch the latest release
tag from intel/intel-device-plugins-for-kubernetes and set appVersion in artifacthub-pkg.yml.
This keeps our Artifact Hub listing in sync with the upstream project.
2026-03-08 12:29:16 +00:00
gandalf-the-greybeard[bot] 231cb41d06 Rename plugin from intel-gpu to headlamp-intel-gpu
Artifact Hub listing was renamed with new repository ID
3c97f78a-26e3-4e8a-89e7-29884602e3d7. Updates package name,
sidebar entries, routes, archive URL, and documentation.

Refs: PRI-26

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 12:14:20 +00:00
hugh-hackman[bot] 0e895c1b61 Merge PR #3
* ci: switch to org-level reusable workflows

* chore: retrigger CI after reusable workflows merged

* feat: add workflow_dispatch to CI workflow

---------

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: hugh-hackman[bot] <266376744+hugh-hackman[bot]@users.noreply.github.com>
Co-authored-by: hugh-hackman[bot] <hugh-hackman[bot]@users.noreply.github.com>
2026-03-08 11:16:24 +00:00
gandalf-the-greybeard[bot] 89e9b510d2 Enhance Renovate configuration (#2)
- Target main branch explicitly
- Set weekly schedule (weekends)
- Limit concurrent PRs to 10
- Group minor/patch updates for npm and github-actions to reduce PR noise

Ref: PRI-16

Co-authored-by: gandalf-the-greybeard[bot] <gandalf-the-greybeard[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 19:01:24 +00:00
Chris Farhood 9d41af375e Merge pull request #1 from privilegedescalation/fix/repo-metadata
chore: add FUNDING.yml
2026-03-07 10:36:41 -05:00
Chris Farhood b0b768783a chore: add FUNDING.yml 2026-03-07 08:03:04 -05:00
DevContainer User c2cbbcc14d docs: add architecture decision records
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:50:00 +00:00
DevContainer User e17875a659 Add artifacthub-headlamp agent skill
Adds Claude Code agent skill for ArtifactHub metadata and publishing,
sourced from headlamp-agent-skills repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:32:14 +00:00
DevContainer User 1ae6e2d355 release: v0.4.1 — code quality fixes and doc updates
Remove unsafe `as any` casts, fix MetricsPage fetch cancellation safety,
delete dead AppBarGpuBadge component, fix typo in data context, move
extractJsonData to module scope, resolve ESLint/Prettier indent conflict,
fix artifacthub-pkg.yml version mismatch and inaccurate description.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:05:58 +00:00
DevContainer User e451e3906e Add headlamp-plugin-developer agent skill
Adds Claude Code agent skill for Headlamp plugin development,
sourced from headlamp-agent-skills repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:26:50 +00:00
24 changed files with 1774 additions and 783 deletions
+241
View File
@@ -0,0 +1,241 @@
---
name: artifacthub-headlamp
description: Use when working with ArtifactHub metadata, releases, or publishing for Headlamp plugins. Covers artifacthub-repo.yml, artifacthub-pkg.yml, Headlamp-specific annotations, and the release-to-publish workflow.
tools: Read, Write, Edit, Glob, Grep, Bash
model: sonnet
---
You are an expert in publishing Headlamp Kubernetes dashboard plugins to ArtifactHub. You understand exactly how ArtifactHub discovers and indexes Headlamp plugins, what metadata is required, and how the release workflow feeds into ArtifactHub listings.
Before editing any metadata files, read the existing `artifacthub-repo.yml`, `artifacthub-pkg.yml`, and `package.json` to understand the current state.
---
## How ArtifactHub Works (Critical Mental Model)
ArtifactHub is a **pull-based, read-only registry**. It periodically scrapes registered GitHub repositories for metadata. There is:
- **NO push API** — you cannot push packages to ArtifactHub
- **NO reconciliation trigger** — you cannot force ArtifactHub to re-scan
- **NO upload endpoint** — tarballs are hosted on GitHub Releases, not ArtifactHub
- **NO webhook integration** — ArtifactHub polls on its own schedule (~30 min)
**The only interface is two YAML files committed to git.** ArtifactHub reads them, and that's it.
---
## Repository Registration
### artifacthub-repo.yml (root of repo)
This file registers the GitHub repository with ArtifactHub. Created once, rarely changed.
```yaml
# Artifact Hub repository metadata file
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml
repositoryID: <uuid> # Assigned by ArtifactHub when you add the repo via the web UI
owners:
- name: <github-username-or-org>
email: <email>
```
**How to get the repositoryID:**
1. Log into artifacthub.io
2. Go to Control Panel → Repositories → Add
3. Select repository kind: "Headlamp plugins"
4. Provide the GitHub repo URL
5. ArtifactHub generates the UUID — copy it into this file
You do NOT generate this UUID yourself. It comes from ArtifactHub's web UI.
---
## Package Metadata
### artifacthub-pkg.yml (root of repo)
This is the primary metadata file that defines how the plugin appears on ArtifactHub. Updated with each release.
```yaml
version: "X.Y.Z" # MUST match package.json version
name: <package-name> # npm package name from package.json
displayName: <Human Readable Name> # Shown on ArtifactHub listing
createdAt: "YYYY-MM-DDTHH:MM:SSZ" # ISO 8601 — update each release
description: >-
Multi-line description of what the plugin does.
Be specific about features and requirements.
license: Apache-2.0
homeURL: https://github.com/<owner>/<repo>
appVersion: "X.Y.Z" # Version of upstream project (optional)
category: <category> # See categories below
keywords:
- headlamp
- kubernetes
- <plugin-specific>
maintainers:
- name: <name>
email: <email>
provider:
name: <name>
links:
- name: GitHub
url: https://github.com/<owner>/<repo>
- name: Issues
url: https://github.com/<owner>/<repo>/issues
changes: # Changelog for this version
- kind: added|changed|fixed|removed
description: "What changed"
annotations: # CRITICAL — Headlamp-specific
headlamp/plugin/archive-url: "https://github.com/<owner>/<repo>/releases/download/v<VERSION>/<pkgname>-<VERSION>.tar.gz"
headlamp/plugin/archive-checksum: "sha256:<checksum>"
headlamp/plugin/version-compat: ">=X.Y.Z"
headlamp/plugin/distro-compat: "<targets>"
```
---
## Headlamp-Specific Annotations (Required)
These annotations in `artifacthub-pkg.yml` are what make ArtifactHub treat the package as a Headlamp plugin:
### headlamp/plugin/archive-url
**Required.** Direct download URL to the plugin tarball on GitHub Releases.
Format: `https://github.com/<owner>/<repo>/releases/download/v<VERSION>/<pkgname>-<VERSION>.tar.gz`
- The tarball is built by `npx @kinvolk/headlamp-plugin build` and then `npx @kinvolk/headlamp-plugin package`
- The `<pkgname>` comes from `package.json` `name` field
- The tarball is uploaded as a GitHub Release asset — NOT to ArtifactHub
### headlamp/plugin/archive-checksum
**Recommended.** SHA256 checksum of the tarball.
Format: `sha256:<hex-digest>`
Generated via: `sha256sum <tarball> | awk '{print $1}'`
Can be empty string if not yet computed (release workflow fills it in).
### headlamp/plugin/version-compat
**Required.** Minimum Headlamp version the plugin works with.
Format: `>=X.Y.Z` (e.g., `>=0.20.0`, `>=0.26`)
### headlamp/plugin/distro-compat
**Required.** Comma-separated list of supported Headlamp deployment targets.
Valid values:
- `in-cluster` — Headlamp running inside a Kubernetes cluster
- `web` — Web-based Headlamp deployment
- `app` — Headlamp desktop application (Electron)
- `desktop` — Alias for desktop app
- `docker-desktop` — Docker Desktop Headlamp extension
Example: `"in-cluster,web,app"`
---
## ArtifactHub Categories
Valid `category` values for Headlamp plugins:
- `security` — Secrets, RBAC, policy enforcement
- `storage` — CSI drivers, persistent volumes, Ceph/Rook
- `monitoring-logging` — Metrics, GPU monitoring, observability
- `networking` — Load balancers, virtual IPs, ingress
---
## Optional Fields
### containersImages
For plugins associated with a specific container/operator:
```yaml
containersImages:
- name: <component-name>
image: docker.io/<org>/<image>:<tag>
```
### recommendations
Link to related ArtifactHub packages:
```yaml
recommendations:
- url: https://artifacthub.io/packages/helm/<repo>/<chart>
```
### install
Custom installation instructions (markdown):
```yaml
install: |
## Install via Headlamp Plugin Manager
...
```
### logoPath
Path to a logo image file in the repo (relative to root).
---
## The Release → ArtifactHub Pipeline
This is the actual flow. There is NO other way to publish:
```
1. Developer triggers release workflow (workflow_dispatch with version)
2. CI runs tests
3. Workflow updates:
- package.json (npm version)
- artifacthub-pkg.yml (version, archive-url, checksum, createdAt, changes)
4. Plugin is built: npx @kinvolk/headlamp-plugin build
5. Plugin is packaged: creates <pkgname>-<version>.tar.gz
6. SHA256 checksum is computed and written to artifacthub-pkg.yml
7. Changes committed to main
8. Git tag created: v<version>
9. GitHub Release created with tarball attached
10. ArtifactHub polls the repo (~30 min) and picks up the new metadata
11. Plugin appears/updates on artifacthub.io
```
**Key points:**
- Steps 1-9 happen in your GitHub Actions workflow
- Step 10 is entirely controlled by ArtifactHub — you cannot trigger it
- The tarball lives on GitHub Releases, not ArtifactHub
- ArtifactHub only reads `artifacthub-pkg.yml` to discover the download URL
---
## Common Mistakes to Avoid
1. **Trying to push/trigger ArtifactHub** — There is no API for this. Just commit metadata and wait.
2. **Version mismatch**`version` in `artifacthub-pkg.yml` MUST match `package.json`. The release workflow should update both.
3. **Wrong archive-url** — Must point to the actual GitHub Release asset URL. Verify the tarball filename matches what the build produces.
4. **Missing checksum** — While optional, missing checksums may cause warnings. The release workflow should compute and write it.
5. **Forgetting createdAt** — Must be updated each release. ArtifactHub uses this for sorting.
6. **Stale changes section** — The `changes` list should reflect the current version's changelog only, not cumulative history.
7. **Assuming ArtifactHub hosts anything** — It's an index/catalog. All artifacts are hosted elsewhere (GitHub Releases).
8. **Trying to generate repositoryID** — This UUID comes from ArtifactHub's web UI when you register the repo. Don't make one up.
---
## Tarball Structure
The plugin tarball built by `@kinvolk/headlamp-plugin` contains:
```
<pkgname>/
main.js # Bundled plugin code
package.json # Plugin metadata
```
The `<pkgname>` directory inside the tarball matches the `name` field from `package.json`.
---
## Validating Metadata
Before committing, check:
1. `version` matches across `package.json` and `artifacthub-pkg.yml`
2. `archive-url` version tag matches the `version` field
3. `name` in `artifacthub-pkg.yml` matches `package.json` `name`
4. `createdAt` is a valid ISO 8601 timestamp
5. All required annotations are present
6. `changes` entries use valid `kind` values: `added`, `changed`, `fixed`, `removed`
+320
View File
@@ -0,0 +1,320 @@
---
name: headlamp-plugin-developer
description: Use when building, extending, debugging, or reviewing Headlamp Kubernetes dashboard plugins. Covers registration APIs, CommonComponents, CRD integration, testing mocks, and codebase conventions.
tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch
model: sonnet
---
You are a senior Headlamp plugin engineer. You produce code matching this codebase's exact conventions. Before writing new code, read `CLAUDE.md` and review existing files in `src/` to understand established patterns.
---
## Plugin Registration Functions
All from `@kinvolk/headlamp-plugin/lib`:
```typescript
registerRoute({
path: string; // React Router path (e.g., '/myresource/:namespace?/:name?')
sidebar?: string; // Sidebar entry name to highlight
component: () => JSX.Element; // Arrow function wrapper required
exact?: boolean;
name?: string; // Used by Link's routeName prop
}): void
registerSidebarEntry({
parent: string | null; // null = top-level
name: string;
label: string;
url: string;
icon?: string; // Iconify ID (e.g., 'mdi:lock')
}): void
registerDetailsViewSection(
(props: { resource: KubeObjectInterface }) => JSX.Element | null
): void
// Runs for ALL resource detail views — MUST check resource?.kind
registerDetailsViewHeaderAction(
(props: { resource: KubeObjectInterface }) => JSX.Element | null
): void
registerResourceTableColumnsProcessor(
(args: { id: string; columns: Column[] }) => Column[]
): void
// id examples: 'headlamp-storageclasses', 'headlamp-persistentvolumes'
registerPluginSettings(
pluginName: string,
component: React.ComponentType<{
data?: Record<string, string | number | boolean>;
onDataChange?: (data: Record<string, string | number | boolean>) => void;
}>,
showSaveButton?: boolean
): void
// Also available but less commonly used:
registerAppBarAction(component): void
registerAppLogo(component): void
registerClusterChooser(component): void
registerSidebarEntryFilter(filter): void
registerRouteFilter(filter): void
registerDetailsViewSectionsProcessor(fn): void
registerHeadlampEventCallback(callback): void
registerAppTheme(theme): void
registerUIPanel(panel): void
```
---
## K8s Module
```typescript
import { K8s } from '@kinvolk/headlamp-plugin/lib';
```
### KubeObject Base Class
```typescript
class KubeObject<T extends KubeObjectInterface> {
jsonData: T; // Raw K8s JSON — use this for spec/status access
metadata: KubeMetadata;
kind: string;
getAge(): string;
getName(): string;
getNamespace(): string | undefined;
delete(force?: boolean): Promise<void>;
patch(body: RecursivePartial<T>): Promise<void>;
static useGet(name?, namespace?): [item: T | null, error: ApiError | null];
static useList(opts?: { namespace?: string }): [items: T[], error: ApiError | null, loading: boolean];
static apiEndpoint: ApiClient | ApiWithNamespaceClient;
static className: string;
}
```
**CRITICAL**: Resource hooks return class instances. Raw K8s JSON lives under `.jsonData`. Access fields via `.jsonData.spec`, `.jsonData.status`, or typed getters.
### ResourceClasses
All standard K8s resource types available (Secret, Namespace, Pod, etc.):
```typescript
const [secrets, error, loading] = K8s.ResourceClasses.Secret.useList({ namespace: 'default' });
const [secret, error] = K8s.ResourceClasses.Secret.useGet('my-secret', 'default');
```
---
## ApiProxy
```typescript
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
ApiProxy.request(
path: string,
options?: {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
body?: string; // JSON.stringify'd
isJSON?: boolean; // false for non-JSON (logs, metrics)
headers?: Record<string, string>;
}
): Promise<unknown>
// CRD endpoint factories
ApiProxy.apiFactoryWithNamespace(group, version, resource): ApiWithNamespaceClient
ApiProxy.apiFactory(group, version, resource): ApiClient
```
**Service proxy URL** (accessing in-cluster services):
```
/api/v1/namespaces/${ns}/services/http:${name}:${port}/proxy${path}
```
---
## CommonComponents
From `@kinvolk/headlamp-plugin/lib/CommonComponents`:
`SectionBox` — container with title and optional `headerProps.actions`
`SectionHeader` — standalone header with title and actions array
`SectionFilterHeader` — header with namespace filter; `noNamespaceFilter` to hide it; `actions` array
`StatusLabel` — status chip; `status`: `'success' | 'error' | 'warning' | 'info'`
`Link` — internal nav; `routeName` + `params` object
`Loader` — spinner with `title` prop
`PercentageBar` — bar chart with `data` array of `{ name, value, fill }`
### SimpleTable (non-obvious props)
```typescript
<SimpleTable
data={items}
columns={[
{ label: 'Name', getter: (item) => item.metadata.name },
{ label: 'Status', getter: (item) => <StatusLabel status="success">Ready</StatusLabel> },
]}
emptyMessage="No items found."
/>
```
### NameValueTable (non-obvious props)
```typescript
<NameValueTable
rows={[
{ name: 'Key', value: 'display value' },
{ name: 'Hidden', value: 'x', hide: true },
]}
/>
```
### ConfigStore
```typescript
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
const store = new ConfigStore<MyConfig>('plugin-name');
store.get(): MyConfig;
store.update(partial: Partial<MyConfig>): void;
store.useConfig(): () => MyConfig;
```
### Pre-bundled (no package.json entry needed)
react, react-dom, react-router-dom, @iconify/react, react-redux, @material-ui/core, @material-ui/styles, lodash, notistack, recharts, monaco-editor
---
## CRD Class Pattern
```typescript
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
const { apiFactoryWithNamespace } = ApiProxy;
const { KubeObject } = K8s.cluster;
type KubeObjectInterface = K8s.cluster.KubeObjectInterface;
interface MyResourceInterface extends KubeObjectInterface {
spec: MySpec;
status?: MyStatus;
}
export class MyResource extends KubeObject<MyResourceInterface> {
static apiEndpoint = apiFactoryWithNamespace('mygroup.io', 'v1', 'myresources');
static get className(): string { return 'MyResource'; }
get spec(): MySpec { return this.jsonData.spec; }
get status(): MyStatus | undefined { return this.jsonData.status; }
}
```
---
## Plugin Entry Point Pattern
```typescript
// 1. Sidebar (parent → children)
registerSidebarEntry({ parent: null, name: 'my-plugin', label: 'My Plugin', icon: 'mdi:icon', url: '/mypath' });
registerSidebarEntry({ parent: 'my-plugin', name: 'my-list', label: 'Resources', url: '/mypath' });
// 2. Routes wrapped in ApiErrorBoundary
registerRoute({
path: '/mypath/:namespace?/:name?',
sidebar: 'my-list',
component: () => <ApiErrorBoundary><MyListPage /></ApiErrorBoundary>,
exact: true, name: 'my-resource',
});
// 3. Detail injection wrapped in GenericErrorBoundary
registerDetailsViewSection(({ resource }) => {
if (resource?.kind !== 'Secret') return null;
return <GenericErrorBoundary><MySection resource={resource} /></GenericErrorBoundary>;
});
// 4. Settings
registerPluginSettings('my-plugin', SettingsPage, true);
```
---
## Headlamp Test Mocks
```typescript
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
ApiProxy: { request: vi.fn().mockResolvedValue({}) },
K8s: { ResourceClasses: {}, cluster: { KubeObject: class {} } },
}));
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
SectionBox: ({ children, title }: any) => <div data-testid="section-box">{title}{children}</div>,
SimpleTable: ({ data, columns }: any) => (
<table><tbody>{data.map((d: any, i: number) =>
<tr key={i}>{columns.map((c: any, j: number) => <td key={j}>{c.getter(d)}</td>)}</tr>
)}</tbody></table>
),
NameValueTable: ({ rows }: any) => (
<dl>{rows.filter((r: any) => !r.hide).map((r: any) =>
<div key={r.name}><dt>{r.name}</dt><dd>{r.value}</dd></div>
)}</dl>
),
StatusLabel: ({ children, status }: any) => <span data-status={status}>{children}</span>,
Link: ({ children }: any) => <a>{children}</a>,
Loader: ({ title }: any) => <div data-testid="loader">{title}</div>,
}));
```
---
## Theming & Dark Mode
Headlamp supports light and dark themes. **Never hardcode colors.** Use CSS custom properties with light-mode fallbacks:
### Required CSS variables for inline styles
```typescript
// Text
color: 'var(--mui-palette-text-primary)'
color: 'var(--mui-palette-text-secondary, #666)'
// Backgrounds
backgroundColor: 'var(--mui-palette-background-default, #fafafa)'
backgroundColor: 'var(--mui-palette-background-paper, #fff)'
// Borders
border: '1px solid var(--mui-palette-divider, #e0e0e0)'
// Interactive
backgroundColor: 'var(--mui-palette-primary-main, #1976d2)'
color: 'var(--mui-palette-primary-contrastText, #fff)'
// Disabled states
backgroundColor: 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
color: 'var(--mui-palette-action-disabled, #9e9e9e)'
// Links
color: 'var(--link-color, #1976d2)'
```
### Common mistakes to avoid
- **NEVER** use raw `#fff`, `#000`, `#333`, `#666` etc. without wrapping in `var(--mui-palette-*)`
- **NEVER** use `rgba(0,0,0,0.5)` for overlays without a variable — this is the one exception where raw rgba is acceptable (backdrop overlays)
- **NEVER** assume white backgrounds or dark text — always use `background-paper`/`text-primary`
- For `<style>` blocks (drawers, etc.), use the same CSS variables in the stylesheet
- Fallback values after the comma are for environments where the variable isn't set — always use the light-mode default
### Form inputs in custom components
```typescript
const inputStyle = {
border: '1px solid var(--mui-palette-divider, #ccc)',
borderRadius: '4px',
backgroundColor: 'var(--mui-palette-background-paper)',
color: 'var(--mui-palette-text-primary)',
};
```
---
## Code Quality Rules
1. **Functional components only** — no class components (except ErrorBoundary)
2. **TypeScript strict mode** — no `any`; use `unknown` + type guards at API boundaries
3. **Headlamp CommonComponents + MUI**`@mui/material` is available via Headlamp's bundled deps; no other UI libraries (no Ant Design, etc.)
4. **Inline CSS only**`style={{}}` props, CSS variables (`var(--mui-palette-*)`) for theming
5. **Accessibility**`aria-label`, `aria-modal`, `role="dialog"`, `aria-live` for dynamic content
6. **Cancellation safety** — async effects must check a `cancelled` flag
7. **Error handling** — Result types in lib/, ErrorBoundaries wrapping components (ApiErrorBoundary for routes, GenericErrorBoundary for injected sections)
8. **Tests** — vitest + @testing-library/react, mock Headlamp APIs per above pattern
9. Run `npm run tsc` and `npm test` after implementation changes
+5
View File
@@ -1,3 +1,8 @@
module.exports = {
extends: ['@headlamp-k8s/eslint-config'],
rules: {
// Prettier handles indentation; the shared config's indent rule
// conflicts with Prettier's JSX ternary formatting.
indent: 'off',
},
};
+1
View File
@@ -0,0 +1 @@
github: [privilegedescalation]
+2 -30
View File
@@ -5,37 +5,9 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
workflow_call:
jobs:
ci:
runs-on: local-ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Lint
run: npm run lint
- name: Type-check
run: npm run tsc
- name: Format check
run: npm run format:check
- name: Run tests
run: npm test
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
+5 -89
View File
@@ -10,95 +10,11 @@ on:
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
pull-requests: write
jobs:
ci:
uses: ./.github/workflows/ci.yaml
release:
needs: ci
runs-on: local-ubuntu-latest
timeout-minutes: 10
steps:
- name: Validate version format
run: |
if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in X.Y.Z format"
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Update version in package.json
run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
- name: Update artifacthub-pkg.yml
run: |
VERSION="${{ inputs.version }}"
PKG_NAME=$(jq -r .name package.json)
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${PKG_NAME}-${VERSION}.tar.gz"
sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npx @kinvolk/headlamp-plugin build
- name: Package plugin
run: npx @kinvolk/headlamp-plugin package
- name: Prepare release tarball
run: |
VERSION="${{ inputs.version }}"
PKG_NAME=$(jq -r .name package.json)
TARBALL="${PKG_NAME}-${VERSION}.tar.gz"
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
- name: Validate tarball
run: |
echo "Tarball: ${{ env.TARBALL }}"
ls -lh "${{ env.TARBALL }}"
tar -tzf "${{ env.TARBALL }}" | head -20
tar -tzf "${{ env.TARBALL }}" | grep -q "main.js" || { echo "Error: main.js not found in tarball"; exit 1; }
- name: Compute checksum
run: |
CHECKSUM=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}')
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
- name: Commit and tag
run: |
VERSION="${{ inputs.version }}"
git add package.json package-lock.json artifacthub-pkg.yml
git commit -m "release: v${VERSION}"
git tag "v${VERSION}"
git push origin main --tags
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ inputs.version }}
name: v${{ inputs.version }}
generate_release_notes: true
files: ${{ env.TARBALL }}
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
with:
version: ${{ inputs.version }}
upstream-repo: 'intel/intel-device-plugins-for-kubernetes'
+2 -2
View File
@@ -35,7 +35,7 @@ src/
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerDetailsViewSection, registerResourceTableColumnsProcessor
├── api/
│ ├── k8s.ts # Types + helpers (GpuDevicePlugin CRD, Nodes, Pods, type guards, formatters)
│ ├── k8s.test.ts # Tests for k8s helpers (70+ test cases)
│ ├── k8s.test.ts # Tests for k8s helpers (48 test cases)
│ ├── metrics.ts # Prometheus GPU power metrics (node-exporter i915 hwmon)
│ └── IntelGpuDataContext.tsx # Shared React context provider with data fetching
└── components/
@@ -44,7 +44,7 @@ src/
├── NodesPage.tsx # Per-node GPU type, device count, allocation, workload pods
├── PodsPage.tsx # All pods requesting Intel GPU resources with per-container detail
├── MetricsPage.tsx # Real-time GPU power metrics from Prometheus
├── NodeDetailSection.tsx # Injected into native Node detail page (capacity, utilization, pods)
├── NodeDetailSection.tsx # Injected into native Node detail page (capacity, utilization, pods)
├── PodDetailSection.tsx # Injected into native Pod detail page (GPU requests per container)
└── integrations/
└── NodeColumns.tsx # GPU Type and GPU Devices columns for native Nodes table
+24
View File
@@ -0,0 +1,24 @@
# Installation Policy
## Approved Installation Method
**The ONLY approved method for installing this plugin is via [Artifact Hub](https://artifacthub.io/) using the Headlamp plugin installer.**
No other installation method is acceptable. This includes but is not limited to:
- Direct installation from GitHub release assets
- Manual npm pack / tarball extraction
- initContainer workarounds that bypass Artifact Hub
- Direct file copy or sidecar injection
## Enforcement
All deployment configurations, CI/CD pipelines, and documentation MUST reference Artifact Hub as the sole plugin distribution channel. Any pull request that introduces an alternative installation method will be rejected.
## Rationale
Artifact Hub provides verified checksums, consistent versioning, and a standard discovery mechanism for the CNCF ecosystem. Bypassing it introduces security and integrity risks.
---
*This policy is set by the CTO and approved by the CEO of Privileged Escalation.*
+1 -23
View File
@@ -18,29 +18,7 @@ A [Headlamp](https://headlamp.dev/) plugin providing visibility into [Intel GPU
## Installation
### Plugin Manager (Headlamp UI)
Search for `intel-gpu` in the Headlamp Plugin Manager.
### Manual
```bash
# Download the latest release tarball
curl -LO https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/latest/download/intel-gpu-*.tar.gz
# Extract to Headlamp plugins directory
mkdir -p ~/.config/Headlamp/plugins
tar -xzf intel-gpu-*.tar.gz -C ~/.config/Headlamp/plugins/
```
### From Source
```bash
git clone https://github.com/privilegedescalation/headlamp-intel-gpu-plugin.git
cd headlamp-intel-gpu-plugin
npm install
npm run build
```
Search for `headlamp-intel-gpu` in the Headlamp Plugin Manager (Settings → Plugins → Catalog).
## Requirements
+19 -29
View File
@@ -1,5 +1,5 @@
version: "0.4.0"
name: intel-gpu
version: "0.4.3"
name: headlamp-intel-gpu
displayName: Intel GPU
description: >-
Headlamp plugin for Intel GPU device plugin visibility and monitoring.
@@ -8,14 +8,14 @@ description: >-
sections into native Node and Pod detail pages. Supports discrete (i915),
Xe, and integrated GPU nodes with graceful degradation when the device
plugin operator is not installed. Includes a Metrics page showing real-time
engine utilization, GPU frequency, VRAM usage, and energy from the device
plugin's Prometheus endpoint.
GPU power draw and TDP from node-exporter i915 hwmon metrics (discrete GPU
nodes only).
createdAt: "2026-02-18T00:00:00Z"
license: Apache-2.0
category: monitoring-logging
homeURL: https://github.com/privilegedescalation/headlamp-intel-gpu-plugin
appVersion: "0.3.0"
appVersion: "0.35.0"
keywords:
- headlamp
@@ -45,33 +45,23 @@ links:
url: https://intel.github.io/intel-device-plugins-for-kubernetes/
changes:
- kind: added
description: "Metrics page: document which metrics require what infrastructure (power via hwmon works out of the box; frequency and utilization need custom exporters)"
- kind: added
description: "Metrics page: real-time GPU power draw (W) and TDP via node-exporter i915 hwmon metrics in kube-prometheus-stack"
- kind: fixed
description: "Remove unsafe `as any` casts in NodeDetailSection"
- kind: fixed
description: "Fix MetricsPage fetch cancellation safety (prevent setState on unmounted component)"
- kind: fixed
description: "Fix typo gpuPluinPods → gpuPluginPods in data context"
- kind: changed
description: "Sidebar label changed to intel-gpu"
description: "Move extractJsonData utility to module scope to avoid recreation on every render"
- kind: removed
description: "Removed app bar health badge"
- kind: added
description: "Overview dashboard: plugin health, GPU node summary, allocation bar, active GPU pods"
- kind: added
description: "Device Plugins page: GpuDevicePlugin CRD instances with spec/status and daemon pods"
- kind: added
description: "GPU Nodes page: per-node GPU type, device count, allocation, workload pods"
- kind: added
description: "GPU Pods page: all pods requesting Intel GPU resources with per-container detail"
- kind: added
description: "Node detail injection: Intel GPU section on native Node detail pages (capacity, allocatable, utilization, active pods)"
- kind: added
description: "Pod detail injection: GPU resource requests/limits per container on native Pod detail pages"
- kind: added
description: "Nodes table: GPU Type and GPU Devices columns injected into native Nodes table"
- kind: added
description: "App bar health badge: hidden when no Intel GPU plugin detected"
description: "Remove dead AppBarGpuBadge component"
- kind: fixed
description: "Fix appVersion mismatch and inaccurate metrics description in Artifact Hub metadata"
- kind: fixed
description: "Resolve ESLint/Prettier indent conflict by disabling ESLint indent rule (Prettier is formatting authority)"
annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.4.0/intel-gpu-0.4.0.tar.gz"
headlamp/plugin/archive-checksum: sha256:f529794d7995b35b954fa32c10874fa8367f6f5cd8040600e47a3013373219df
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.4.3/intel-gpu-0.4.3.tar.gz"
headlamp/plugin/archive-checksum: sha256:d9c78b3d678d3e6b92c81315bfed88bd22ec4f5cd63578467206727244db7dab
headlamp/plugin/version-compat: ">=0.20.0"
headlamp/plugin/distro-compat: "in-cluster,web,app"
+1 -1
View File
@@ -1,5 +1,5 @@
# Artifact Hub repository metadata
repositoryID: c927788f-9d34-49d9-a18c-e6f78951bdfd
repositoryID: 3c97f78a-26e3-4e8a-89e7-29884602e3d7
owners:
- name: privilegedescalation
@@ -0,0 +1,52 @@
# ADR 001: React Context for Centralized GPU State
**Status**: Accepted
**Date**: 2026-03-05
**Deciders**: Development Team
---
## Context
The Intel GPU plugin needs to share GPU-related data across 5 page views (Overview, DevicePlugins, Nodes, Pods, Metrics) and 2 detail view sections (Node, Pod). Data includes GPU nodes (identified by node labels and capacity fields), GPU pods, GpuDevicePlugin CRD instances, and plugin DaemonSet pods.
The `IntelGpuDataProvider` context holds all derived GPU state. Child components access data via `useIntelGpuContext()`. The context collects errors from three streams (node hook error, pod hook error, async CRD fetch error) into a `string[]` joined with `';'` into a single error string.
---
## Decision
Use a single `IntelGpuDataProvider` React Context that wraps every route and every `registerDetailsViewSection` call in `index.tsx`. All GPU-derived state is computed in the provider and exposed via context.
---
## Consequences
- ✅ Single source of truth for all GPU data
- ✅ All views share consistent state
- ✅ Error aggregation from multiple sources into a unified error string
- ✅ Refresh mechanism updates everything atomically
- ⚠️ All consumers re-render on any data change
- ⚠️ Monolithic provider couples all GPU state together
The negative consequences are mitigated by the fact that GPU data updates infrequently in practice, so unnecessary re-renders are rare.
---
## Alternatives Considered
1. **Per-page data fetching** — Rejected. Would duplicate complex GPU node/pod filtering logic across each of the 5 pages and 2 detail sections.
2. **Multiple contexts (NodesContext, PodsContext, CRDContext)** — Rejected. GPU data is highly cross-referenced (e.g., GPU pods reference GPU nodes, CRD instances relate to DaemonSet pods). Splitting contexts would require complex cross-context coordination.
3. **External state library (Redux, Zustand, etc.)** — Rejected. External state libraries are not available in the Headlamp plugin runtime environment.
---
## Changelog
| Date | Change |
|------|--------|
| 2026-03-05 | Initial decision accepted |
@@ -0,0 +1,59 @@
# ADR 002: Dual Data Fetching Strategy (Hooks + ApiProxy)
**Status**: Accepted
**Date**: 2026-03-05
**Deciders**: Development Team
---
## Context
The plugin needs data from two categories of Kubernetes resources:
- **Standard resources**: Nodes and Pods, for which Headlamp provides reactive `useList()` hooks via built-in resource classes.
- **Custom resources**: GpuDevicePlugin CRD (under `deviceplugin.intel.com/v1`) and DaemonSet pods with specific labels, for which Headlamp does not have built-in support.
Headlamp provides reactive `useList()` hooks for standard resource classes but does not have built-in support for custom CRDs. The plugin uses three possible label selectors for DaemonSet pod discovery to handle different deployment configurations.
---
## Decision
Implement a two-track data fetching strategy within the context provider:
1. **Track 1 (Reactive)**: Use `K8s.ResourceClasses.Node.useList()` and `K8s.ResourceClasses.Pod.useList({namespace:''})` for standard resources. These are reactive to cluster changes and automatically update when resources are created, modified, or deleted.
2. **Track 2 (Imperative)**: Use `ApiProxy.request()` inside a `useEffect` keyed on `refreshKey` for GpuDevicePlugin CRDs and DaemonSet pods. The `refreshKey` is incremented by the `refresh()` function exposed through the context.
---
## Consequences
- ✅ Leverages Headlamp's reactive hooks for standard resources with automatic updates
- ✅ Flexible `ApiProxy` for custom CRDs without needing to register custom resource classes
- ✅ Refresh mechanism provides manual control over imperative fetches
- ✅ Clean separation of reactive vs imperative data sources
- ⚠️ Two different update mechanisms (hooks auto-update vs manual refresh for CRDs)
- ⚠️ CRD data may lag behind hook data between refreshes
The negative consequences are mitigated by providing a manual refresh button in the UI, allowing users to force an update of imperative data when needed.
---
## Alternatives Considered
1. **All ApiProxy (no hooks)** — Rejected. Loses reactivity for standard resources, meaning Node and Pod changes would not be reflected until a manual refresh.
2. **All hooks (register CRD as custom resource class)** — Rejected. Headlamp's `KubeObject` registration is complex for read-only CRD access and would add unnecessary coupling to Headlamp internals.
3. **Single useEffect for everything** — Rejected. Loses the reactivity benefit for Nodes and Pods, and would require manual refresh for all data instead of just CRDs.
---
## Changelog
| Date | Change |
|------|--------|
| 2026-03-05 | Initial decision accepted |
@@ -0,0 +1,53 @@
# ADR 003: Graceful CRD Degradation
**Status**: Accepted
**Date**: 2026-03-05
**Deciders**: Development Team
---
## Context
The GpuDevicePlugin CRD (`deviceplugin.intel.com/v1`) is only present when the Intel GPU device plugin operator is installed. However, Intel GPUs can be present in a cluster without the operator — the device plugin can be deployed as a plain DaemonSet.
The plugin should still detect and display GPU resources even without the CRD. GPU nodes are identifiable by node labels (e.g., `intel.feature.node.kubernetes.io/gpu`) and capacity fields (e.g., `gpu.intel.com/i915`). GPU pods are identifiable by resource requests/limits for Intel GPU resources.
---
## Decision
Wrap the GpuDevicePlugin CRD fetch in its own `try/catch`. If the fetch fails (CRD not installed), set `crdAvailable` to `false` and continue. GPU nodes and pods are still discovered via node labels, capacity fields, and pod resource requests — independent of the CRD.
The CRD data enriches the view when available but is not required for core functionality.
---
## Consequences
- ✅ Plugin works on any cluster with Intel GPUs regardless of operator installation
- ✅ Progressive enhancement when CRD is available
- ✅ No error displayed to the user for a missing CRD
- ⚠️ Two code paths (with/without CRD data) increase testing surface
- ⚠️ DevicePlugins page is empty without the CRD
The negative consequences are mitigated by clear messaging on the DevicePlugins page when the CRD is unavailable, informing users that the operator is not installed.
---
## Alternatives Considered
1. **Require CRD (hard dependency)** — Rejected. Too restrictive; many clusters run the device plugin as a plain DaemonSet without the operator and its CRD.
2. **API discovery check before fetch** — Considered, but `try/catch` is simpler and handles all failure modes (CRD not installed, API server errors, permission issues) uniformly.
3. **Disable plugin entirely without CRD** — Rejected. Core GPU monitoring (node detection, pod resource tracking) works without the CRD and provides significant value on its own.
---
## Changelog
| Date | Change |
|------|--------|
| 2026-03-05 | Initial decision accepted |
@@ -0,0 +1,61 @@
# ADR 004: Headlamp View Integration via Detail Sections and Column Processors
**Status**: Accepted
**Date**: 2026-03-05
**Deciders**: Development Team
---
## Context
The plugin provides its own pages (Overview, Nodes, Pods, etc.) but also needs to enhance Headlamp's native views. Users browsing the standard Nodes list should see GPU information without navigating to the plugin.
Headlamp offers two integration mechanisms:
- `registerDetailsViewSection` for injecting sections into resource detail pages.
- `registerResourceTableColumnsProcessor` for adding columns to resource list tables.
---
## Decision
Use both integration mechanisms:
1. **Detail sections**: `registerDetailsViewSection` injects GPU information into Node and Pod detail pages. Resource-kind guards ensure sections only render for the correct resource type.
2. **Column processors**: `registerResourceTableColumnsProcessor` appends "GPU Type" and "GPU Devices" columns to the native `headlamp-nodes` table.
Both integration points consume data from the shared `IntelGpuDataProvider` context, so they benefit from the same cached data as the plugin's own pages.
---
## Consequences
- ✅ GPU data visible in native Headlamp views without navigation
- ✅ Seamless user experience for users already familiar with Headlamp
- ✅ Uses Headlamp's official extension APIs for forward compatibility
- ✅ Shared context means no duplicate data fetches
- ⚠️ Detail sections render for all Nodes/Pods (guard needed to check GPU relevance)
- ⚠️ Column processors add columns even when no GPU nodes exist in the cluster
The negative consequences are mitigated by resource-kind guards and conditional rendering that hide GPU sections when a resource has no GPU relevance.
---
## Alternatives Considered
1. **Plugin pages only (no native view integration)** — Rejected. Users would miss GPU info when browsing standard Headlamp views, reducing discoverability.
2. **Override native views entirely** — Rejected. Not supported by Headlamp's plugin API and would conflict with other plugins.
3. **App bar notification only** — Rejected. Insufficient detail for node-level and pod-level GPU information; only suitable for cluster-wide summaries.
---
## Changelog
| Date | Change |
|------|--------|
| 2026-03-05 | Initial decision accepted |
+42
View File
@@ -0,0 +1,42 @@
# Architecture Decision Records
## What is an ADR?
An Architecture Decision Record (ADR) captures an important architectural decision made along with its context and consequences. ADRs are used to document the reasoning behind significant technical choices so that future contributors can understand why the system is built the way it is.
## Format
This project follows the [Nygard-style ADR format](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions):
- **Title**: Short noun phrase describing the decision
- **Status**: Proposed, Accepted, Deprecated, or Superseded
- **Date**: When the decision was made
- **Deciders**: Who was involved in making the decision
- **Context**: What is the issue that motivated the decision
- **Decision**: What is the change that was decided
- **Consequences**: What becomes easier or more difficult as a result
- **Alternatives Considered**: What other options were evaluated
## Index
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [001](001-react-context-state.md) | React Context for Centralized GPU State | Accepted | 2026-03-05 |
| [002](002-dual-data-fetching.md) | Dual Data Fetching Strategy (Hooks + ApiProxy) | Accepted | 2026-03-05 |
| [003](003-graceful-crd-degradation.md) | Graceful CRD Degradation | Accepted | 2026-03-05 |
| [004](004-native-view-integration.md) | Headlamp View Integration via Detail Sections and Column Processors | Accepted | 2026-03-05 |
## Creating New ADRs
1. Copy an existing ADR as a template.
2. Assign the next sequential number (e.g., `005`).
3. Fill in all sections: Status, Date, Deciders, Context, Decision, Consequences, and Alternatives Considered.
4. Set the status to `Proposed` until the team reviews and accepts the decision.
5. Update this README index table with the new entry.
6. Submit as part of a pull request for team review.
## References
- [Michael Nygard - Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions)
- [ADR GitHub Organization](https://adr.github.io/)
- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/)
+802 -526
View File
File diff suppressed because it is too large Load Diff
+18 -2
View File
@@ -1,6 +1,6 @@
{
"name": "intel-gpu",
"version": "0.4.0",
"version": "0.4.3",
"description": "Headlamp plugin for Intel GPU device plugin visibility and monitoring",
"repository": {
"type": "git",
@@ -24,7 +24,23 @@
"test": "vitest run",
"test:watch": "vitest"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0"
"@kinvolk/headlamp-plugin": "^0.13.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"jsdom": "^24.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^5.3.0",
"vitest": "^3.0.5"
},
"overrides": {
"tar": "^7.5.11",
"undici": "^7.24.3"
}
}
+16 -1
View File
@@ -1,4 +1,19 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
"extends": ["config:recommended"],
"baseBranches": ["main"],
"schedule": ["every weekend"],
"prConcurrentLimit": 10,
"packageRules": [
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "npm minor and patch"
},
{
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "github-actions minor and patch"
}
]
}
+14 -9
View File
@@ -65,6 +65,18 @@ export function useIntelGpuContext(): IntelGpuContextValue {
return ctx;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Extract raw Kubernetes JSON from Headlamp KubeObject wrappers. */
const extractJsonData = (items: unknown[]): unknown[] =>
items.map(item =>
item && typeof item === 'object' && 'jsonData' in item
? (item as { jsonData: unknown }).jsonData
: item
);
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
@@ -129,8 +141,8 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
try {
const list = await ApiProxy.request(url);
if (!cancelled && isKubeList(list)) {
const gpuPluinPods = filterIntelGpuPluginPods(list.items);
foundPluginPods.push(...gpuPluinPods);
const gpuPluginPods = filterIntelGpuPluginPods(list.items);
foundPluginPods.push(...gpuPluginPods);
}
} catch {
// Silently ignore — some selectors may not match
@@ -170,13 +182,6 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
// 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[]));
-46
View File
@@ -1,46 +0,0 @@
/**
* AppBarGpuBadge — compact Intel GPU health indicator in the Headlamp app bar.
*
* Shows a status chip in the top navigation bar summarising GPU plugin health.
* Hides itself when no Intel GPU plugin is detected.
*/
import { StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
export default function AppBarGpuBadge() {
const { pluginInstalled, gpuNodes, devicePlugins, loading } = useIntelGpuContext();
// Hide when loading or no plugin present
if (loading || !pluginInstalled) return null;
const hasUnhealthyPlugin = devicePlugins.some(p => {
const desired = p.status?.desiredNumberScheduled ?? 0;
const ready = p.status?.numberReady ?? 0;
const unavailable = p.status?.numberUnavailable ?? 0;
return (desired > 0 && ready < desired) || unavailable > 0;
});
const status = hasUnhealthyPlugin ? 'warning' : 'success';
const nodeCount = gpuNodes.length;
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '0 8px',
cursor: 'default',
}}
title={`Intel GPU: ${nodeCount} node${nodeCount !== 1 ? 's' : ''}`}
>
<StatusLabel status={status}>
<span style={{ fontSize: '11px', fontWeight: 600 }}>
Intel GPU{nodeCount > 0 ? ` · ${nodeCount}N` : ''}
</span>
</StatusLabel>
</div>
);
}
+31 -20
View File
@@ -194,30 +194,41 @@ export default function MetricsPage() {
const [metrics, setMetrics] = useState<GpuMetrics | null>(null);
const [fetchError, setFetchError] = useState<string | null>(null);
const [fetching, setFetching] = useState(false);
const [fetchSeq, setFetchSeq] = useState(0);
const doFetch = useCallback(async () => {
setFetching(true);
setFetchError(null);
try {
const result = await fetchGpuMetrics();
setMetrics(result);
if (!result) {
setFetchError(
'Could not reach Prometheus. Ensure kube-prometheus-stack is installed in the monitoring namespace.'
);
}
} catch (e: unknown) {
setFetchError(e instanceof Error ? e.message : String(e));
} finally {
setFetching(false);
}
const doFetch = useCallback(() => {
setFetchSeq(s => s + 1);
}, []);
useEffect(() => {
if (!ctxLoading) {
void doFetch();
}
}, [ctxLoading, doFetch]);
if (ctxLoading) return;
let cancelled = false;
setFetching(true);
setFetchError(null);
fetchGpuMetrics()
.then(result => {
if (cancelled) return;
setMetrics(result);
if (!result) {
setFetchError(
'Could not reach Prometheus. Ensure kube-prometheus-stack is installed in the monitoring namespace.'
);
}
})
.catch((e: unknown) => {
if (cancelled) return;
setFetchError(e instanceof Error ? e.message : String(e));
})
.finally(() => {
if (!cancelled) setFetching(false);
});
return () => {
cancelled = true;
};
}, [ctxLoading, fetchSeq]);
if (ctxLoading) {
return <Loader title="Loading Intel GPU data..." />;
+4 -4
View File
@@ -52,11 +52,11 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
metadata: { name: string; labels?: Record<string, string> };
};
const nodeName = (node as { metadata: { name: string } }).metadata.name;
const capacity = getGpuResources((node as any).status?.capacity);
const allocatable = getGpuResources((node as any).status?.allocatable);
const nodeName = node.metadata.name;
const capacity = getGpuResources(node.status?.capacity);
const allocatable = getGpuResources(node.status?.allocatable);
const gpuType = getNodeGpuType(node as any);
const gpuType = getNodeGpuType(node);
// Find GPU pods scheduled on this node
const podsOnNode = loading ? [] : gpuPods.filter(p => p.spec?.nodeName === nodeName);
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* headlamp-intel-gpu-plugin — entry point.
* intel-gpu-plugin — entry point.
*
* Registers sidebar entries, routes, detail view sections, and table column
* processors for Intel GPU device plugin visibility in Headlamp.