Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db896a8f88 | |||
| a16df9baf7 | |||
| 865168285e | |||
| 84af42147f | |||
| b0de53577a | |||
| 231cb41d06 | |||
| 0e895c1b61 | |||
| 89e9b510d2 | |||
| 9d41af375e | |||
| b0b768783a | |||
| c2cbbcc14d | |||
| e17875a659 | |||
| 1ae6e2d355 | |||
| e451e3906e | |||
| 01b60a23b8 | |||
| 488bf90abc | |||
| 034e0b9db8 |
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: agent-installer
|
||||||
|
description: Use this agent when the user wants to discover, browse, or install Claude Code agents from the awesome-claude-code-subagents repository.
|
||||||
|
tools: Bash, WebFetch, Read, Write, Glob
|
||||||
|
model: haiku
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an agent installer that helps users browse and install Claude Code agents from the awesome-claude-code-subagents repository on GitHub.
|
||||||
|
|
||||||
|
## Your Capabilities
|
||||||
|
|
||||||
|
You can:
|
||||||
|
1. List all available agent categories
|
||||||
|
2. List agents within a category
|
||||||
|
3. Search for agents by name or description
|
||||||
|
4. Install agents to global (~/.claude/agents/) or local (.claude/agents/) directory
|
||||||
|
5. Show details about a specific agent before installing
|
||||||
|
6. Uninstall agents
|
||||||
|
|
||||||
|
## GitHub API Endpoints
|
||||||
|
|
||||||
|
- Categories list: `https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents/categories`
|
||||||
|
- Agents in category: `https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents/categories/{category-name}`
|
||||||
|
- Raw agent file: `https://raw.githubusercontent.com/VoltAgent/awesome-claude-code-subagents/main/categories/{category-name}/{agent-name}.md`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### When user asks to browse or list agents:
|
||||||
|
1. Fetch categories from GitHub API using WebFetch or Bash with curl
|
||||||
|
2. Parse the JSON response to extract directory names
|
||||||
|
3. Present categories in a numbered list
|
||||||
|
4. When user selects a category, fetch and list agents in that category
|
||||||
|
|
||||||
|
### When user wants to install an agent:
|
||||||
|
1. Ask if they want global installation (~/.claude/agents/) or local (.claude/agents/)
|
||||||
|
2. For local: Check if .claude/ directory exists, create .claude/agents/ if needed
|
||||||
|
3. Download the agent .md file from GitHub raw URL
|
||||||
|
4. Save to the appropriate directory
|
||||||
|
5. Confirm successful installation
|
||||||
|
|
||||||
|
### When user wants to search:
|
||||||
|
1. Fetch the README.md which contains all agent listings
|
||||||
|
2. Search for the term in agent names and descriptions
|
||||||
|
3. Present matching results
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: agent-organizer
|
||||||
|
description: Use when assembling and optimizing multi-agent teams to execute complex projects that require careful task decomposition, agent capability matching, and workflow coordination.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior agent organizer with expertise in assembling and coordinating multi-agent teams. Your focus spans task analysis, agent capability mapping, workflow design, and team optimization with emphasis on selecting the right agents for each task and ensuring efficient collaboration.
|
||||||
|
|
||||||
|
When invoked:
|
||||||
|
1. Query context manager for task requirements and available agents
|
||||||
|
2. Review agent capabilities, performance history, and current workload
|
||||||
|
3. Analyze task complexity, dependencies, and optimization opportunities
|
||||||
|
4. Orchestrate agent teams for maximum efficiency and success
|
||||||
|
|
||||||
|
Agent organization checklist:
|
||||||
|
- Agent selection accuracy > 95% achieved
|
||||||
|
- Task completion rate > 99% maintained
|
||||||
|
- Resource utilization optimal consistently
|
||||||
|
- Response time < 5s ensured
|
||||||
|
- Error recovery automated properly
|
||||||
|
- Cost tracking enabled thoroughly
|
||||||
|
- Performance monitored continuously
|
||||||
|
- Team synergy maximized effectively
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: multi-agent-coordinator
|
||||||
|
description: Use when coordinating multiple concurrent agents that need to communicate, share state, synchronize work, and handle distributed failures across a system.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep
|
||||||
|
model: opus
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior multi-agent coordinator with expertise in orchestrating complex distributed workflows. Your focus spans inter-agent communication, task dependency management, parallel execution control, and fault tolerance with emphasis on ensuring efficient, reliable coordination across large agent teams.
|
||||||
|
|
||||||
|
When invoked:
|
||||||
|
1. Query context manager for workflow requirements and agent states
|
||||||
|
2. Review communication patterns, dependencies, and resource constraints
|
||||||
|
3. Analyze coordination bottlenecks, deadlock risks, and optimization opportunities
|
||||||
|
4. Implement robust multi-agent coordination strategies
|
||||||
|
|
||||||
|
Multi-agent coordination checklist:
|
||||||
|
- Coordination overhead < 5% maintained
|
||||||
|
- Deadlock prevention 100% ensured
|
||||||
|
- Message delivery guaranteed thoroughly
|
||||||
|
- Scalability to 100+ agents verified
|
||||||
|
- Fault tolerance built-in properly
|
||||||
|
- Monitoring comprehensive continuously
|
||||||
|
- Recovery automated effectively
|
||||||
|
- Performance optimal consistently
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(done)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)",
|
||||||
|
"Bash(git push:*)",
|
||||||
|
"Bash(gh workflow:*)",
|
||||||
|
"Bash(gh run:*)",
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"Bash(npm ci:*)",
|
||||||
|
"Bash(npm test:*)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enabledMcpjsonServers": [
|
||||||
|
"github",
|
||||||
|
"kubernetes",
|
||||||
|
"flux",
|
||||||
|
"playwright"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +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',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
github: [privilegedescalation]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Release version (e.g. 1.0.0)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
|
||||||
|
with:
|
||||||
|
version: ${{ inputs.version }}
|
||||||
|
upstream-repo: 'intel/intel-device-plugins-for-kubernetes'
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"github": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://api.githubcopilot.com/mcp/",
|
||||||
|
"headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" }
|
||||||
|
},
|
||||||
|
"kubernetes": { "type": "sse", "url": "http://localhost:8080/sse" },
|
||||||
|
"flux": { "type": "sse", "url": "http://localhost:8081/sse" },
|
||||||
|
"playwright": { "type": "sse", "url": "http://localhost:8086/sse" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('@headlamp-k8s/eslint-config/prettier-config');
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
Headlamp plugin for Intel GPU device plugin visibility and monitoring. Read-only — monitors GpuDevicePlugin CRDs, GPU-capable nodes, pods requesting Intel GPU resources, and real-time power metrics via Prometheus. No cluster write operations.
|
||||||
|
|
||||||
|
- **Plugin name**: `headlamp-intel-gpu`
|
||||||
|
- **Target**: Headlamp >= v0.20.0
|
||||||
|
- **Data sources**: GpuDevicePlugin CRDs (`deviceplugin.intel.com/v1`), Nodes, Pods (all namespaces), Prometheus (node-exporter i915 hwmon)
|
||||||
|
- **Reference plugin**: `../headlamp-kube-vip-plugin`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # dev server with hot reload
|
||||||
|
npm run build # production build
|
||||||
|
npm run package # package for headlamp
|
||||||
|
npm run tsc # TypeScript type check (no emit)
|
||||||
|
npm run lint # ESLint
|
||||||
|
npm run lint:fix # ESLint with auto-fix
|
||||||
|
npm run format # Prettier write
|
||||||
|
npm run format:check # Prettier check
|
||||||
|
npm test # vitest run
|
||||||
|
npm run test:watch # vitest watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
All tests and `tsc` must pass before committing.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
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 (48 test cases)
|
||||||
|
│ ├── metrics.ts # Prometheus GPU power metrics (node-exporter i915 hwmon)
|
||||||
|
│ └── IntelGpuDataContext.tsx # Shared React context provider with data fetching
|
||||||
|
└── components/
|
||||||
|
├── OverviewPage.tsx # Dashboard: plugin health, GPU node summary, allocation, active pods
|
||||||
|
├── DevicePluginsPage.tsx # GpuDevicePlugin CRD instances with spec/status and daemon pods
|
||||||
|
├── 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)
|
||||||
|
├── 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
`IntelGpuDataContext.tsx` uses **two fetching strategies**:
|
||||||
|
|
||||||
|
1. **Headlamp hooks** (`K8s.ResourceClasses.*.useList()`) — for Nodes and Pods.
|
||||||
|
2. **`ApiProxy.request()`** — for GpuDevicePlugin CRDs and plugin daemon pods (with label selector fallback).
|
||||||
|
|
||||||
|
The plugin gracefully degrades when the GpuDevicePlugin CRD is not installed — GPU nodes and pods are still shown based on resource labels and capacity.
|
||||||
|
|
||||||
|
## Key constants (src/api/k8s.ts)
|
||||||
|
|
||||||
|
- API group: `deviceplugin.intel.com`
|
||||||
|
- API version: `v1`
|
||||||
|
- GPU resources: `gpu.intel.com/i915`, `gpu.intel.com/xe`, `gpu.intel.com/millicores`, `gpu.intel.com/memory.max`
|
||||||
|
- Resource prefix: `gpu.intel.com/`
|
||||||
|
- Node labels: `intel.feature.node.kubernetes.io/gpu`, `node-role.kubernetes.io/gpu`, `node-role.kubernetes.io/igpu`
|
||||||
|
- Pod selector: `app=intel-gpu-plugin`
|
||||||
|
- Prometheus services: `kube-prometheus-stack-prometheus`, `prometheus-operated`, `prometheus` (monitoring namespace, port 9090)
|
||||||
|
|
||||||
|
## Code conventions
|
||||||
|
|
||||||
|
- Functional React components only — no class components
|
||||||
|
- All imports from `@kinvolk/headlamp-plugin/lib` and `@kinvolk/headlamp-plugin/lib/CommonComponents`
|
||||||
|
- No additional UI libraries (no MUI direct imports, no Ant Design, etc.)
|
||||||
|
- TypeScript strict mode — no `any`, use `unknown` + type guards at API boundaries
|
||||||
|
- Context provider (`IntelGpuDataProvider`) wraps each route component in `index.tsx`
|
||||||
|
- Tests: vitest + @testing-library/react, mock with `vi.mock('@kinvolk/headlamp-plugin/lib', ...)`
|
||||||
|
- `vitest.setup.ts` provides a spec-compliant `localStorage` shim for Node 22+ compatibility
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Mock pattern for headlamp APIs:
|
||||||
|
```typescript
|
||||||
|
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||||
|
ApiProxy: { request: vi.fn().mockResolvedValue({ items: [] }) },
|
||||||
|
K8s: {
|
||||||
|
ResourceClasses: {
|
||||||
|
Node: { useList: vi.fn(() => [[], null]) },
|
||||||
|
Pod: { useList: vi.fn(() => [[], null]) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please follow these guidelines.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/privilegedescalation/headlamp-intel-gpu-plugin.git
|
||||||
|
cd headlamp-intel-gpu-plugin
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Before Submitting a PR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run tsc # TypeScript type check
|
||||||
|
npm run lint # ESLint
|
||||||
|
npm run format:check # Prettier
|
||||||
|
npm test # All tests must pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- TypeScript strict mode (no `any`)
|
||||||
|
- Functional React components only
|
||||||
|
- All UI from `@kinvolk/headlamp-plugin/lib/CommonComponents`
|
||||||
|
- Tests with vitest + @testing-library/react
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Use conventional commit format:
|
||||||
|
- `feat:` new features
|
||||||
|
- `fix:` bug fixes
|
||||||
|
- `chore:` maintenance
|
||||||
|
- `docs:` documentation
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by the Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding any notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2025 privilegedescalation
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# headlamp-intel-gpu-plugin
|
||||||
|
|
||||||
|
[](https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/actions/workflows/ci.yaml)
|
||||||
|
[](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
|
||||||
|
A [Headlamp](https://headlamp.dev/) plugin providing visibility into [Intel GPU device plugin](https://intel.github.io/intel-device-plugins-for-kubernetes/) deployments on Kubernetes.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Overview Dashboard** — Plugin health, GPU node summary, allocation bar, active GPU pods
|
||||||
|
- **Device Plugins** — GpuDevicePlugin CRD instances with spec/status and daemon pod health
|
||||||
|
- **GPU Nodes** — Per-node GPU type (discrete/integrated), device count, allocation, workload pods
|
||||||
|
- **GPU Pods** — All pods requesting Intel GPU resources with per-container detail
|
||||||
|
- **Metrics** — Real-time GPU power draw (W) and TDP via Prometheus node-exporter i915 hwmon
|
||||||
|
- **Node Detail Integration** — Intel GPU section injected into native Headlamp Node detail views
|
||||||
|
- **Pod Detail Integration** — GPU resource requests/limits injected into native Pod detail views
|
||||||
|
- **Nodes Table Columns** — GPU Type and GPU Devices columns added to native Nodes table
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Plugin Manager (Headlamp UI)
|
||||||
|
|
||||||
|
Search for `headlamp-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/headlamp-intel-gpu-*.tar.gz
|
||||||
|
|
||||||
|
# Extract to Headlamp plugins directory
|
||||||
|
mkdir -p ~/.config/Headlamp/plugins
|
||||||
|
tar -xzf headlamp-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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Headlamp >= v0.20.0
|
||||||
|
- Intel GPU device plugin deployed (optional — plugin gracefully degrades without it)
|
||||||
|
- Optional: Node Feature Discovery with Intel GPU labels
|
||||||
|
- Optional: kube-prometheus-stack with node-exporter for GPU power metrics
|
||||||
|
|
||||||
|
## RBAC
|
||||||
|
|
||||||
|
This plugin is **read-only** and requires the following permissions:
|
||||||
|
|
||||||
|
| Resource | API Group | Verbs |
|
||||||
|
|----------|-----------|-------|
|
||||||
|
| nodes | v1 | list, get, watch |
|
||||||
|
| pods | v1 | list, get, watch |
|
||||||
|
| gpudeviceplugins | deviceplugin.intel.com/v1 | list, get |
|
||||||
|
|
||||||
|
For metrics, Prometheus must be accessible via the Headlamp API proxy in the `monitoring` namespace.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.tsx # Plugin entry point
|
||||||
|
├── api/
|
||||||
|
│ ├── k8s.ts # Types and helper functions
|
||||||
|
│ ├── metrics.ts # Prometheus GPU metrics
|
||||||
|
│ └── IntelGpuDataContext.tsx # React context provider
|
||||||
|
└── components/
|
||||||
|
├── OverviewPage.tsx # Dashboard
|
||||||
|
├── DevicePluginsPage.tsx # Device plugin CRDs
|
||||||
|
├── NodesPage.tsx # GPU nodes
|
||||||
|
├── PodsPage.tsx # GPU pods
|
||||||
|
├── MetricsPage.tsx # Power metrics
|
||||||
|
├── NodeDetailSection.tsx # Injected into Node detail view
|
||||||
|
├── PodDetailSection.tsx # Injected into Pod detail view
|
||||||
|
└── integrations/
|
||||||
|
└── NodeColumns.tsx # Nodes table columns
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start # dev server
|
||||||
|
npm test # run tests
|
||||||
|
npm run tsc # type check
|
||||||
|
npm run lint # ESLint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| No GPU nodes shown | No Intel GPU labels or resources on nodes | Install Intel Node Feature Discovery or Intel GPU device plugin |
|
||||||
|
| CRD not available warning | GpuDevicePlugin CRD not installed | Install Intel device plugins operator — plugin still works without it |
|
||||||
|
| No metrics data | Prometheus not found | Deploy kube-prometheus-stack in the `monitoring` namespace |
|
||||||
|
| Metrics show only discrete GPUs | Integrated GPUs lack hwmon | Expected — iGPU driver doesn't expose hwmon power data |
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License 2.0. See [LICENSE](LICENSE) for details.
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
|---------|-----------|
|
||||||
|
| latest | Yes |
|
||||||
|
|
||||||
|
## Plugin Scope
|
||||||
|
|
||||||
|
This plugin is **read-only**. It does not perform any write operations against the Kubernetes cluster. It reads:
|
||||||
|
|
||||||
|
- Nodes
|
||||||
|
- Pods (all namespaces)
|
||||||
|
- GpuDevicePlugin CRDs (`deviceplugin.intel.com/v1`)
|
||||||
|
- Prometheus metrics (via API proxy in `monitoring` namespace)
|
||||||
|
|
||||||
|
All data is fetched through Headlamp's built-in API proxy, which respects the user's existing RBAC permissions.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report security vulnerabilities by opening a private issue or emailing the maintainers directly.
|
||||||
+19
-29
@@ -1,5 +1,5 @@
|
|||||||
version: "0.3.0"
|
version: "0.4.2"
|
||||||
name: headlamp-intel-gpu-plugin
|
name: headlamp-intel-gpu
|
||||||
displayName: Intel GPU
|
displayName: Intel GPU
|
||||||
description: >-
|
description: >-
|
||||||
Headlamp plugin for Intel GPU device plugin visibility and monitoring.
|
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),
|
sections into native Node and Pod detail pages. Supports discrete (i915),
|
||||||
Xe, and integrated GPU nodes with graceful degradation when the device
|
Xe, and integrated GPU nodes with graceful degradation when the device
|
||||||
plugin operator is not installed. Includes a Metrics page showing real-time
|
plugin operator is not installed. Includes a Metrics page showing real-time
|
||||||
engine utilization, GPU frequency, VRAM usage, and energy from the device
|
GPU power draw and TDP from node-exporter i915 hwmon metrics (discrete GPU
|
||||||
plugin's Prometheus endpoint.
|
nodes only).
|
||||||
createdAt: "2026-02-18T00:00:00Z"
|
createdAt: "2026-02-18T00:00:00Z"
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
category: monitoring-logging
|
category: monitoring-logging
|
||||||
|
|
||||||
homeURL: https://github.com/privilegedescalation/headlamp-intel-gpu-plugin
|
homeURL: https://github.com/privilegedescalation/headlamp-intel-gpu-plugin
|
||||||
appVersion: "0.3.0"
|
appVersion: "0.35.0"
|
||||||
|
|
||||||
keywords:
|
keywords:
|
||||||
- headlamp
|
- headlamp
|
||||||
@@ -45,33 +45,23 @@ links:
|
|||||||
url: https://intel.github.io/intel-device-plugins-for-kubernetes/
|
url: https://intel.github.io/intel-device-plugins-for-kubernetes/
|
||||||
|
|
||||||
changes:
|
changes:
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Metrics page: document which metrics require what infrastructure (power via hwmon works out of the box; frequency and utilization need custom exporters)"
|
description: "Remove unsafe `as any` casts in NodeDetailSection"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Metrics page: real-time GPU power draw (W) and TDP via node-exporter i915 hwmon metrics in kube-prometheus-stack"
|
description: "Fix MetricsPage fetch cancellation safety (prevent setState on unmounted component)"
|
||||||
|
- kind: fixed
|
||||||
|
description: "Fix typo gpuPluinPods → gpuPluginPods in data context"
|
||||||
- kind: changed
|
- kind: changed
|
||||||
description: "Sidebar label changed to intel-gpu"
|
description: "Move extractJsonData utility to module scope to avoid recreation on every render"
|
||||||
- kind: removed
|
- kind: removed
|
||||||
description: "Removed app bar health badge"
|
description: "Remove dead AppBarGpuBadge component"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Overview dashboard: plugin health, GPU node summary, allocation bar, active GPU pods"
|
description: "Fix appVersion mismatch and inaccurate metrics description in Artifact Hub metadata"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Device Plugins page: GpuDevicePlugin CRD instances with spec/status and daemon pods"
|
description: "Resolve ESLint/Prettier indent conflict by disabling ESLint indent rule (Prettier is formatting authority)"
|
||||||
- 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"
|
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.3.0/headlamp-intel-gpu-plugin-0.3.0.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.4.2/headlamp-intel-gpu-0.4.2.tar.gz"
|
||||||
headlamp/plugin/archive-checksum: "sha256:fdc53099ee3123680f24fe4a319b753ca3d030aac31abd4e3f383221085c9c2d"
|
headlamp/plugin/archive-checksum: sha256:0713b099a79ed63ea30675fee96f2a70e37471507d4135b529df158a09960492
|
||||||
headlamp/plugin/version-compat: ">=0.20.0"
|
headlamp/plugin/version-compat: ">=0.20.0"
|
||||||
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Artifact Hub repository metadata
|
# Artifact Hub repository metadata
|
||||||
repositoryID: c927788f-9d34-49d9-a18c-e6f78951bdfd
|
repositoryID: 3c97f78a-26e3-4e8a-89e7-29884602e3d7
|
||||||
|
|
||||||
owners:
|
owners:
|
||||||
- name: privilegedescalation
|
- 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 |
|
||||||
@@ -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/)
|
||||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-intel-gpu-plugin",
|
"name": "intel-gpu",
|
||||||
"version": "0.1.0",
|
"version": "0.4.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "headlamp-intel-gpu-plugin",
|
"name": "intel-gpu",
|
||||||
"version": "0.1.0",
|
"version": "0.4.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||||
|
|||||||
+8
-4
@@ -1,12 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-intel-gpu-plugin",
|
"name": "headlamp-intel-gpu",
|
||||||
"version": "0.3.0",
|
"version": "0.4.2",
|
||||||
"description": "Headlamp plugin for Intel GPU device plugin visibility and monitoring",
|
"description": "Headlamp plugin for Intel GPU device plugin visibility and monitoring",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/cpfarhood/headlamp-intel-gpu-plugin.git"
|
"url": "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin.git"
|
||||||
},
|
},
|
||||||
"author": "cpfarhood",
|
"bugs": {
|
||||||
|
"url": "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin#readme",
|
||||||
|
"author": "privilegedescalation",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "headlamp-plugin start",
|
"start": "headlamp-plugin start",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -65,6 +65,18 @@ export function useIntelGpuContext(): IntelGpuContextValue {
|
|||||||
return ctx;
|
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
|
// Provider
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -116,9 +128,11 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// Intel device plugins operator deployment
|
// Intel device plugins operator deployment
|
||||||
`/api/v1/pods?labelSelector=${encodeURIComponent('app=intel-gpu-plugin')}`,
|
`/api/v1/pods?labelSelector=${encodeURIComponent('app=intel-gpu-plugin')}`,
|
||||||
// Alternative: by component label
|
// Alternative: by component label
|
||||||
`/api/v1/pods?labelSelector=${encodeURIComponent('app.kubernetes.io/name=intel-gpu-plugin')}`,
|
`/api/v1/pods?labelSelector=${encodeURIComponent(
|
||||||
|
'app.kubernetes.io/name=intel-gpu-plugin'
|
||||||
|
)}`,
|
||||||
// Intel device plugins from inteldeviceplugins-system namespace
|
// Intel device plugins from inteldeviceplugins-system namespace
|
||||||
`/api/v1/namespaces/inteldeviceplugins-system/pods`,
|
'/api/v1/namespaces/inteldeviceplugins-system/pods',
|
||||||
];
|
];
|
||||||
|
|
||||||
const foundPluginPods: IntelGpuPod[] = [];
|
const foundPluginPods: IntelGpuPod[] = [];
|
||||||
@@ -127,8 +141,8 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
|||||||
try {
|
try {
|
||||||
const list = await ApiProxy.request(url);
|
const list = await ApiProxy.request(url);
|
||||||
if (!cancelled && isKubeList(list)) {
|
if (!cancelled && isKubeList(list)) {
|
||||||
const gpuPluinPods = filterIntelGpuPluginPods(list.items);
|
const gpuPluginPods = filterIntelGpuPluginPods(list.items);
|
||||||
foundPluginPods.push(...gpuPluinPods);
|
foundPluginPods.push(...gpuPluginPods);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Silently ignore — some selectors may not match
|
// Silently ignore — some selectors may not match
|
||||||
@@ -155,7 +169,9 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
|||||||
}
|
}
|
||||||
|
|
||||||
void fetchAsync();
|
void fetchAsync();
|
||||||
return () => { cancelled = true; };
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
}, [refreshKey]);
|
}, [refreshKey]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -166,13 +182,6 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// type helpers work correctly.
|
// 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(() => {
|
const gpuNodes = useMemo(() => {
|
||||||
if (!allNodes) return [];
|
if (!allNodes) return [];
|
||||||
return filterIntelGpuNodes(extractJsonData(allNodes as unknown[]));
|
return filterIntelGpuNodes(extractJsonData(allNodes as unknown[]));
|
||||||
|
|||||||
+4
-8
@@ -12,18 +12,18 @@ import {
|
|||||||
getNodeGpuCount,
|
getNodeGpuCount,
|
||||||
getNodeGpuType,
|
getNodeGpuType,
|
||||||
getPodGpuRequests,
|
getPodGpuRequests,
|
||||||
|
type GpuDevicePlugin,
|
||||||
INTEL_GPU_NODE_LABEL,
|
INTEL_GPU_NODE_LABEL,
|
||||||
INTEL_GPU_RESOURCE,
|
INTEL_GPU_RESOURCE,
|
||||||
INTEL_GPU_XE_RESOURCE,
|
INTEL_GPU_XE_RESOURCE,
|
||||||
|
type IntelGpuNode,
|
||||||
|
type IntelGpuPod,
|
||||||
isGpuRequestingPod,
|
isGpuRequestingPod,
|
||||||
isIntelGpuNode,
|
isIntelGpuNode,
|
||||||
isKubeList,
|
isKubeList,
|
||||||
isNodeReady,
|
isNodeReady,
|
||||||
pluginStatusText,
|
pluginStatusText,
|
||||||
pluginStatusToStatus,
|
pluginStatusToStatus,
|
||||||
type GpuDevicePlugin,
|
|
||||||
type IntelGpuNode,
|
|
||||||
type IntelGpuPod,
|
|
||||||
} from './k8s';
|
} from './k8s';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -413,11 +413,7 @@ describe('formatGpuType', () => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe('pluginStatusToStatus', () => {
|
describe('pluginStatusToStatus', () => {
|
||||||
function makePlugin(
|
function makePlugin(desired: number, ready: number, unavailable = 0): GpuDevicePlugin {
|
||||||
desired: number,
|
|
||||||
ready: number,
|
|
||||||
unavailable = 0
|
|
||||||
): GpuDevicePlugin {
|
|
||||||
return {
|
return {
|
||||||
apiVersion: 'deviceplugin.intel.com/v1',
|
apiVersion: 'deviceplugin.intel.com/v1',
|
||||||
kind: 'GpuDevicePlugin',
|
kind: 'GpuDevicePlugin',
|
||||||
|
|||||||
+21
-28
@@ -28,8 +28,7 @@ export const INTEL_DISCRETE_GPU_NODE_ROLE = 'node-role.kubernetes.io/gpu';
|
|||||||
export const INTEL_INTEGRATED_GPU_NODE_ROLE = 'node-role.kubernetes.io/igpu';
|
export const INTEL_INTEGRATED_GPU_NODE_ROLE = 'node-role.kubernetes.io/igpu';
|
||||||
|
|
||||||
/** Label selector for Intel GPU device plugin DaemonSet pods */
|
/** Label selector for Intel GPU device plugin DaemonSet pods */
|
||||||
export const INTEL_GPU_PLUGIN_LABEL_SELECTOR =
|
export const INTEL_GPU_PLUGIN_LABEL_SELECTOR = 'app=intel-gpu-plugin';
|
||||||
'app=intel-gpu-plugin';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Generic Kubernetes object base shapes
|
// Generic Kubernetes object base shapes
|
||||||
@@ -194,9 +193,12 @@ export function getNodeGpuType(node: IntelGpuNode): GpuType {
|
|||||||
|
|
||||||
export function formatGpuType(type: GpuType): string {
|
export function formatGpuType(type: GpuType): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'discrete': return 'Discrete';
|
case 'discrete':
|
||||||
case 'integrated': return 'Integrated';
|
return 'Discrete';
|
||||||
default: return 'Unknown';
|
case 'integrated':
|
||||||
|
return 'Integrated';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,9 +274,11 @@ export function isIntelGpuPluginPod(pod: unknown): pod is IntelGpuPod {
|
|||||||
const meta = obj['metadata'] as Record<string, unknown> | undefined;
|
const meta = obj['metadata'] as Record<string, unknown> | undefined;
|
||||||
const labels = meta?.['labels'] as Record<string, string> | undefined;
|
const labels = meta?.['labels'] as Record<string, string> | undefined;
|
||||||
if (!labels) return false;
|
if (!labels) return false;
|
||||||
return labels['app'] === 'intel-gpu-plugin' ||
|
return (
|
||||||
(labels['app.kubernetes.io/name'] === 'intel-gpu-plugin') ||
|
labels['app'] === 'intel-gpu-plugin' ||
|
||||||
(labels['component'] === 'intel-gpu-plugin');
|
labels['app.kubernetes.io/name'] === 'intel-gpu-plugin' ||
|
||||||
|
labels['component'] === 'intel-gpu-plugin'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterIntelGpuPluginPods(items: unknown[]): IntelGpuPod[] {
|
export function filterIntelGpuPluginPods(items: unknown[]): IntelGpuPod[] {
|
||||||
@@ -284,10 +288,7 @@ export function filterIntelGpuPluginPods(items: unknown[]): IntelGpuPod[] {
|
|||||||
/** Get total GPU requests from a pod's containers */
|
/** Get total GPU requests from a pod's containers */
|
||||||
export function getPodGpuRequests(pod: IntelGpuPod): Record<string, string> {
|
export function getPodGpuRequests(pod: IntelGpuPod): Record<string, string> {
|
||||||
const totals: Record<string, number> = {};
|
const totals: Record<string, number> = {};
|
||||||
const allContainers = [
|
const allContainers = [...(pod.spec?.containers ?? []), ...(pod.spec?.initContainers ?? [])];
|
||||||
...(pod.spec?.containers ?? []),
|
|
||||||
...(pod.spec?.initContainers ?? []),
|
|
||||||
];
|
|
||||||
for (const c of allContainers) {
|
for (const c of allContainers) {
|
||||||
const requests = c.resources?.requests ?? {};
|
const requests = c.resources?.requests ?? {};
|
||||||
for (const [key, value] of Object.entries(requests)) {
|
for (const [key, value] of Object.entries(requests)) {
|
||||||
@@ -300,15 +301,11 @@ export function getPodGpuRequests(pod: IntelGpuPod): Record<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isPodReady(pod: IntelGpuPod): boolean {
|
export function isPodReady(pod: IntelGpuPod): boolean {
|
||||||
return (
|
return pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
|
||||||
pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPodRestarts(pod: IntelGpuPod): number {
|
export function getPodRestarts(pod: IntelGpuPod): number {
|
||||||
return (
|
return pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0;
|
||||||
pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -330,9 +327,7 @@ export function isKubeList(value: unknown): value is KubeList<unknown> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function isNodeReady(node: IntelGpuNode): boolean {
|
export function isNodeReady(node: IntelGpuNode): boolean {
|
||||||
return (
|
return node.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
|
||||||
node.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -359,11 +354,11 @@ export function formatAge(timestamp: string | undefined): string {
|
|||||||
export function formatGpuResourceName(resourceKey: string): string {
|
export function formatGpuResourceName(resourceKey: string): string {
|
||||||
const name = resourceKey.replace(INTEL_GPU_RESOURCE_PREFIX, '');
|
const name = resourceKey.replace(INTEL_GPU_RESOURCE_PREFIX, '');
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'i915': 'GPU (i915)',
|
i915: 'GPU (i915)',
|
||||||
'xe': 'GPU (Xe)',
|
xe: 'GPU (Xe)',
|
||||||
'millicores': 'GPU Millicores',
|
millicores: 'GPU Millicores',
|
||||||
'memory.max': 'GPU Memory (max)',
|
'memory.max': 'GPU Memory (max)',
|
||||||
'tiles': 'GPU Tiles',
|
tiles: 'GPU Tiles',
|
||||||
};
|
};
|
||||||
return map[name] ?? name;
|
return map[name] ?? name;
|
||||||
}
|
}
|
||||||
@@ -372,9 +367,7 @@ export function formatGpuResourceName(resourceKey: string): string {
|
|||||||
// Status helpers
|
// Status helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function pluginStatusToStatus(
|
export function pluginStatusToStatus(plugin: GpuDevicePlugin): 'success' | 'warning' | 'error' {
|
||||||
plugin: GpuDevicePlugin
|
|
||||||
): 'success' | 'warning' | 'error' {
|
|
||||||
const desired = plugin.status?.desiredNumberScheduled ?? 0;
|
const desired = plugin.status?.desiredNumberScheduled ?? 0;
|
||||||
const ready = plugin.status?.numberReady ?? 0;
|
const ready = plugin.status?.numberReady ?? 0;
|
||||||
const unavailable = plugin.status?.numberUnavailable ?? 0;
|
const unavailable = plugin.status?.numberUnavailable ?? 0;
|
||||||
|
|||||||
+5
-6
@@ -64,14 +64,11 @@ const PROMETHEUS_SERVICES = [
|
|||||||
{ namespace: 'monitoring', service: 'prometheus', port: '9090' },
|
{ namespace: 'monitoring', service: 'prometheus', port: '9090' },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function queryPrometheus(
|
async function queryPrometheus(query: string, prometheusPath: string): Promise<PrometheusResult[]> {
|
||||||
query: string,
|
|
||||||
prometheusPath: string
|
|
||||||
): Promise<PrometheusResult[]> {
|
|
||||||
const encoded = encodeURIComponent(query);
|
const encoded = encodeURIComponent(query);
|
||||||
const path = `${prometheusPath}/api/v1/query?query=${encoded}`;
|
const path = `${prometheusPath}/api/v1/query?query=${encoded}`;
|
||||||
|
|
||||||
const raw = await ApiProxy.request(path, { method: 'GET' }) as PrometheusResponse;
|
const raw = (await ApiProxy.request(path, { method: 'GET' })) as PrometheusResponse;
|
||||||
|
|
||||||
if (raw?.status !== 'success') return [];
|
if (raw?.status !== 'success') return [];
|
||||||
return raw.data?.result ?? [];
|
return raw.data?.result ?? [];
|
||||||
@@ -81,7 +78,9 @@ async function findPrometheusPath(): Promise<string | null> {
|
|||||||
for (const { namespace, service, port } of PROMETHEUS_SERVICES) {
|
for (const { namespace, service, port } of PROMETHEUS_SERVICES) {
|
||||||
const basePath = `/api/v1/namespaces/${namespace}/services/${service}:${port}/proxy`;
|
const basePath = `/api/v1/namespaces/${namespace}/services/${service}:${port}/proxy`;
|
||||||
try {
|
try {
|
||||||
const raw = await ApiProxy.request(`${basePath}/api/v1/query?query=1`, { method: 'GET' }) as PrometheusResponse;
|
const raw = (await ApiProxy.request(`${basePath}/api/v1/query?query=1`, {
|
||||||
|
method: 'GET',
|
||||||
|
})) as PrometheusResponse;
|
||||||
if (raw?.status === 'success') return basePath;
|
if (raw?.status === 'success') return basePath;
|
||||||
} catch {
|
} catch {
|
||||||
// try next
|
// try next
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,7 @@ import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
|||||||
import { formatAge, isPodReady, pluginStatusText, pluginStatusToStatus } from '../api/k8s';
|
import { formatAge, isPodReady, pluginStatusText, pluginStatusToStatus } from '../api/k8s';
|
||||||
|
|
||||||
export default function DevicePluginsPage() {
|
export default function DevicePluginsPage() {
|
||||||
const { devicePlugins, pluginPods, crdAvailable, loading, error, refresh } =
|
const { devicePlugins, pluginPods, crdAvailable, loading, error, refresh } = useIntelGpuContext();
|
||||||
useIntelGpuContext();
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader title="Loading device plugin data..." />;
|
return <Loader title="Loading device plugin data..." />;
|
||||||
@@ -27,7 +26,14 @@ export default function DevicePluginsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SectionHeader title="Intel GPU — Device Plugins" />
|
<SectionHeader title="Intel GPU — Device Plugins" />
|
||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
@@ -102,7 +108,10 @@ export default function DevicePluginsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{devicePlugins.map(plugin => (
|
{devicePlugins.map(plugin => (
|
||||||
<SectionBox key={plugin.metadata.uid ?? plugin.metadata.name} title={`GpuDevicePlugin: ${plugin.metadata.name}`}>
|
<SectionBox
|
||||||
|
key={plugin.metadata.uid ?? plugin.metadata.name}
|
||||||
|
title={`GpuDevicePlugin: ${plugin.metadata.name}`}
|
||||||
|
>
|
||||||
<NameValueTable
|
<NameValueTable
|
||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
@@ -146,14 +155,14 @@ export default function DevicePluginsPage() {
|
|||||||
value: String(plugin.status?.numberReady ?? '—'),
|
value: String(plugin.status?.numberReady ?? '—'),
|
||||||
},
|
},
|
||||||
...(plugin.status?.numberUnavailable
|
...(plugin.status?.numberUnavailable
|
||||||
? [{
|
? [
|
||||||
name: 'Unavailable Nodes',
|
{
|
||||||
value: (
|
name: 'Unavailable Nodes',
|
||||||
<StatusLabel status="error">
|
value: (
|
||||||
{plugin.status.numberUnavailable}
|
<StatusLabel status="error">{plugin.status.numberUnavailable}</StatusLabel>
|
||||||
</StatusLabel>
|
),
|
||||||
),
|
},
|
||||||
}]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
name: 'Node Selector',
|
name: 'Node Selector',
|
||||||
@@ -177,12 +186,12 @@ export default function DevicePluginsPage() {
|
|||||||
<SectionBox title="Plugin Daemon Pods">
|
<SectionBox title="Plugin Daemon Pods">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
{ label: 'Name', getter: p => p.metadata.name },
|
||||||
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
|
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
|
||||||
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
|
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
|
||||||
{
|
{
|
||||||
label: 'Ready',
|
label: 'Ready',
|
||||||
getter: (p) => (
|
getter: p => (
|
||||||
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
|
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
|
||||||
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
|
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
|
||||||
</StatusLabel>
|
</StatusLabel>
|
||||||
@@ -190,10 +199,9 @@ export default function DevicePluginsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Restarts',
|
label: 'Restarts',
|
||||||
getter: (p) => {
|
getter: p => {
|
||||||
const restarts = p.status?.containerStatuses?.reduce(
|
const restarts =
|
||||||
(sum, c) => sum + c.restartCount, 0
|
p.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0;
|
||||||
) ?? 0;
|
|
||||||
return restarts > 0 ? (
|
return restarts > 0 ? (
|
||||||
<StatusLabel status="warning">{restarts}</StatusLabel>
|
<StatusLabel status="warning">{restarts}</StatusLabel>
|
||||||
) : (
|
) : (
|
||||||
@@ -201,7 +209,7 @@ export default function DevicePluginsPage() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={pluginPods}
|
data={pluginPods}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -35,7 +35,13 @@ import {
|
|||||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||||
import { fetchGpuMetrics, formatPercent, formatWatts, GpuChipMetrics, GpuMetrics } from '../api/metrics';
|
import {
|
||||||
|
fetchGpuMetrics,
|
||||||
|
formatPercent,
|
||||||
|
formatWatts,
|
||||||
|
GpuChipMetrics,
|
||||||
|
GpuMetrics,
|
||||||
|
} from '../api/metrics';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Power bar
|
// Power bar
|
||||||
@@ -43,7 +49,8 @@ import { fetchGpuMetrics, formatPercent, formatWatts, GpuChipMetrics, GpuMetrics
|
|||||||
|
|
||||||
function PowerBar({ watts, maxWatts }: { watts: number; maxWatts: number | null }) {
|
function PowerBar({ watts, maxWatts }: { watts: number; maxWatts: number | null }) {
|
||||||
const pct = maxWatts && maxWatts > 0 ? Math.min(100, Math.round((watts / maxWatts) * 100)) : null;
|
const pct = maxWatts && maxWatts > 0 ? Math.min(100, Math.round((watts / maxWatts) * 100)) : null;
|
||||||
const color = pct === null ? '#0071c5' : pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
|
const color =
|
||||||
|
pct === null ? '#0071c5' : pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
@@ -91,9 +98,12 @@ function GpuChipCard({ chip }: { chip: GpuChipMetrics }) {
|
|||||||
{ name: 'GPU (PCI)', value: chip.chip },
|
{ name: 'GPU (PCI)', value: chip.chip },
|
||||||
{
|
{
|
||||||
name: 'Current Power',
|
name: 'Current Power',
|
||||||
value: chip.powerWatts !== null
|
value:
|
||||||
? <PowerBar watts={chip.powerWatts} maxWatts={chip.powerMaxWatts} />
|
chip.powerWatts !== null ? (
|
||||||
: <StatusLabel status="warning">No data — needs ≥5m of scrape history</StatusLabel>,
|
<PowerBar watts={chip.powerWatts} maxWatts={chip.powerMaxWatts} />
|
||||||
|
) : (
|
||||||
|
<StatusLabel status="warning">No data — needs ≥5m of scrape history</StatusLabel>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -123,8 +133,9 @@ function MetricRequirements() {
|
|||||||
<>
|
<>
|
||||||
<StatusLabel status="success">Available — discrete GPU nodes</StatusLabel>
|
<StatusLabel status="success">Available — discrete GPU nodes</StatusLabel>
|
||||||
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
||||||
Source: <code>node_hwmon_energy_joule_total</code> via node-exporter hwmon collector (enabled by default).
|
Source: <code>node_hwmon_energy_joule_total</code> via node-exporter hwmon
|
||||||
Requires the i915 kernel driver on the node. iGPU nodes do not expose hwmon sensors.
|
collector (enabled by default). Requires the i915 kernel driver on the node. iGPU
|
||||||
|
nodes do not expose hwmon sensors.
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -136,8 +147,9 @@ function MetricRequirements() {
|
|||||||
<StatusLabel status="error">Not available</StatusLabel>
|
<StatusLabel status="error">Not available</StatusLabel>
|
||||||
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
||||||
i915 exposes <code>gt_*_freq_mhz</code> via DRM sysfs but node-exporter's{' '}
|
i915 exposes <code>gt_*_freq_mhz</code> via DRM sysfs but node-exporter's{' '}
|
||||||
<code>--collector.drm</code> flag is AMD-only and does not read these files.
|
<code>--collector.drm</code> flag is AMD-only and does not read these files. A
|
||||||
A custom exporter or textfile-collector sidecar writing these values would be required.
|
custom exporter or textfile-collector sidecar writing these values would be
|
||||||
|
required.
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -148,8 +160,8 @@ function MetricRequirements() {
|
|||||||
<>
|
<>
|
||||||
<StatusLabel status="error">Not available</StatusLabel>
|
<StatusLabel status="error">Not available</StatusLabel>
|
||||||
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
||||||
No standard Prometheus collector exposes i915 engine busy percentage.
|
No standard Prometheus collector exposes i915 engine busy percentage. Would
|
||||||
Would require intel-gpu-top, XPU Manager, or a custom DRM-based exporter.
|
require intel-gpu-top, XPU Manager, or a custom DRM-based exporter.
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -160,8 +172,8 @@ function MetricRequirements() {
|
|||||||
<>
|
<>
|
||||||
<StatusLabel status="error">No metrics available</StatusLabel>
|
<StatusLabel status="error">No metrics available</StatusLabel>
|
||||||
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
|
||||||
The integrated GPU driver does not expose hwmon sensors. No Prometheus metrics
|
The integrated GPU driver does not expose hwmon sensors. No Prometheus metrics are
|
||||||
are available for iGPU nodes regardless of configuration.
|
available for iGPU nodes regardless of configuration.
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -182,28 +194,41 @@ export default function MetricsPage() {
|
|||||||
const [metrics, setMetrics] = useState<GpuMetrics | null>(null);
|
const [metrics, setMetrics] = useState<GpuMetrics | null>(null);
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
|
const [fetchSeq, setFetchSeq] = useState(0);
|
||||||
|
|
||||||
const doFetch = useCallback(async () => {
|
const doFetch = useCallback(() => {
|
||||||
setFetching(true);
|
setFetchSeq(s => s + 1);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ctxLoading) {
|
if (ctxLoading) return;
|
||||||
void doFetch();
|
|
||||||
}
|
let cancelled = false;
|
||||||
}, [ctxLoading, doFetch]);
|
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) {
|
if (ctxLoading) {
|
||||||
return <Loader title="Loading Intel GPU data..." />;
|
return <Loader title="Loading Intel GPU data..." />;
|
||||||
@@ -211,7 +236,14 @@ export default function MetricsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SectionHeader title="Intel GPU — Metrics" />
|
<SectionHeader title="Intel GPU — Metrics" />
|
||||||
<button
|
<button
|
||||||
onClick={() => void doFetch()}
|
onClick={() => void doFetch()}
|
||||||
@@ -246,7 +278,8 @@ export default function MetricsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Checked services',
|
name: 'Checked services',
|
||||||
value: 'kube-prometheus-stack-prometheus:9090, prometheus-operated:9090, prometheus:9090 (monitoring namespace)',
|
value:
|
||||||
|
'kube-prometheus-stack-prometheus:9090, prometheus-operated:9090, prometheus:9090 (monitoring namespace)',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -261,17 +294,22 @@ export default function MetricsPage() {
|
|||||||
name: 'Status',
|
name: 'Status',
|
||||||
value: (
|
value: (
|
||||||
<StatusLabel status="warning">
|
<StatusLabel status="warning">
|
||||||
Prometheus reachable — no node_hwmon_chip_names{chip_name="i915"} found
|
Prometheus reachable — no
|
||||||
|
node_hwmon_chip_names{chip_name="i915"} found
|
||||||
</StatusLabel>
|
</StatusLabel>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'GPU Nodes',
|
name: 'GPU Nodes',
|
||||||
value: gpuNodes.length > 0 ? gpuNodes.map(n => n.metadata.name).join(', ') : 'None detected',
|
value:
|
||||||
|
gpuNodes.length > 0
|
||||||
|
? gpuNodes.map(n => n.metadata.name).join(', ')
|
||||||
|
: 'None detected',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Likely cause',
|
name: 'Likely cause',
|
||||||
value: 'node-exporter is not running on the GPU nodes, or the hwmon collector is disabled.',
|
value:
|
||||||
|
'node-exporter is not running on the GPU nodes, or the hwmon collector is disabled.',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -301,7 +339,8 @@ export default function MetricsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Query',
|
name: 'Query',
|
||||||
value: 'rate(node_hwmon_energy_joule_total[5m]) joined with node_hwmon_chip_names{chip_name="i915"}',
|
value:
|
||||||
|
'rate(node_hwmon_energy_joule_total[5m]) joined with node_hwmon_chip_names{chip_name="i915"}',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ import {
|
|||||||
getGpuResources,
|
getGpuResources,
|
||||||
getNodeGpuType,
|
getNodeGpuType,
|
||||||
INTEL_GPU_RESOURCE,
|
INTEL_GPU_RESOURCE,
|
||||||
INTEL_GPU_RESOURCE_PREFIX,
|
|
||||||
INTEL_GPU_XE_RESOURCE,
|
INTEL_GPU_XE_RESOURCE,
|
||||||
isIntelGpuNode,
|
isIntelGpuNode,
|
||||||
isNodeReady,
|
|
||||||
} from '../api/k8s';
|
} from '../api/k8s';
|
||||||
|
|
||||||
interface NodeDetailSectionProps {
|
interface NodeDetailSectionProps {
|
||||||
@@ -40,9 +38,7 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
|
|||||||
|
|
||||||
// Extract the raw Kubernetes JSON — Headlamp KubeObject wraps it in jsonData
|
// Extract the raw Kubernetes JSON — Headlamp KubeObject wraps it in jsonData
|
||||||
const rawNode =
|
const rawNode =
|
||||||
resource.jsonData && typeof resource.jsonData === 'object'
|
resource.jsonData && typeof resource.jsonData === 'object' ? resource.jsonData : resource;
|
||||||
? resource.jsonData
|
|
||||||
: resource;
|
|
||||||
|
|
||||||
// Only render for Node resources that have Intel GPU
|
// Only render for Node resources that have Intel GPU
|
||||||
if (!isIntelGpuNode(rawNode)) return null;
|
if (!isIntelGpuNode(rawNode)) return null;
|
||||||
@@ -56,16 +52,14 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
|
|||||||
metadata: { name: string; labels?: Record<string, string> };
|
metadata: { name: string; labels?: Record<string, string> };
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeName = (node as { metadata: { name: string } }).metadata.name;
|
const nodeName = node.metadata.name;
|
||||||
const capacity = getGpuResources((node as any).status?.capacity);
|
const capacity = getGpuResources(node.status?.capacity);
|
||||||
const allocatable = getGpuResources((node as any).status?.allocatable);
|
const allocatable = getGpuResources(node.status?.allocatable);
|
||||||
|
|
||||||
const gpuType = getNodeGpuType(node as any);
|
const gpuType = getNodeGpuType(node);
|
||||||
|
|
||||||
// Find GPU pods scheduled on this node
|
// Find GPU pods scheduled on this node
|
||||||
const podsOnNode = loading
|
const podsOnNode = loading ? [] : gpuPods.filter(p => p.spec?.nodeName === nodeName);
|
||||||
? []
|
|
||||||
: gpuPods.filter(p => p.spec?.nodeName === nodeName);
|
|
||||||
|
|
||||||
if (Object.keys(capacity).length === 0 && Object.keys(allocatable).length === 0) {
|
if (Object.keys(capacity).length === 0 && Object.keys(allocatable).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -81,18 +75,18 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const pod of podsOnNode.filter(p => p.status?.phase === 'Running')) {
|
for (const pod of podsOnNode.filter(p => p.status?.phase === 'Running')) {
|
||||||
const reqs = pod.spec?.containers?.flatMap(c =>
|
const reqs =
|
||||||
Object.entries(c.resources?.requests ?? {}).filter(([k]) =>
|
pod.spec?.containers?.flatMap(c =>
|
||||||
k === INTEL_GPU_RESOURCE || k === INTEL_GPU_XE_RESOURCE
|
Object.entries(c.resources?.requests ?? {}).filter(
|
||||||
)
|
([k]) => k === INTEL_GPU_RESOURCE || k === INTEL_GPU_XE_RESOURCE
|
||||||
) ?? [];
|
)
|
||||||
|
) ?? [];
|
||||||
for (const [, val] of reqs) {
|
for (const [, val] of reqs) {
|
||||||
gpuInUse += parseInt(val, 10) || 0;
|
gpuInUse += parseInt(val, 10) || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const utilizationPct =
|
const utilizationPct = gpuAllocatable > 0 ? Math.round((gpuInUse / gpuAllocatable) * 100) : 0;
|
||||||
gpuAllocatable > 0 ? Math.round((gpuInUse / gpuAllocatable) * 100) : 0;
|
|
||||||
const utilizationStatus: 'success' | 'warning' | 'error' =
|
const utilizationStatus: 'success' | 'warning' | 'error' =
|
||||||
utilizationPct >= 90 ? 'error' : utilizationPct >= 70 ? 'warning' : 'success';
|
utilizationPct >= 90 ? 'error' : utilizationPct >= 70 ? 'warning' : 'success';
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
getNodeGpuCount,
|
getNodeGpuCount,
|
||||||
getNodeGpuType,
|
getNodeGpuType,
|
||||||
INTEL_GPU_RESOURCE,
|
INTEL_GPU_RESOURCE,
|
||||||
INTEL_GPU_RESOURCE_PREFIX,
|
|
||||||
INTEL_GPU_XE_RESOURCE,
|
INTEL_GPU_XE_RESOURCE,
|
||||||
IntelGpuNode,
|
IntelGpuNode,
|
||||||
isNodeReady,
|
isNodeReady,
|
||||||
@@ -33,13 +32,7 @@ import {
|
|||||||
// GPU allocation bar component
|
// GPU allocation bar component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function GpuAllocationBar({
|
function GpuAllocationBar({ used, allocatable }: { used: number; allocatable: number }) {
|
||||||
used,
|
|
||||||
allocatable,
|
|
||||||
}: {
|
|
||||||
used: number;
|
|
||||||
allocatable: number;
|
|
||||||
}) {
|
|
||||||
if (allocatable === 0) return <span>—</span>;
|
if (allocatable === 0) return <span>—</span>;
|
||||||
const pct = Math.min(100, Math.round((used / allocatable) * 100));
|
const pct = Math.min(100, Math.round((used / allocatable) * 100));
|
||||||
const color = pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
|
const color = pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
|
||||||
@@ -105,21 +98,18 @@ function NodeDetailCard({
|
|||||||
name: 'GPU Type',
|
name: 'GPU Type',
|
||||||
value: formatGpuType(gpuType),
|
value: formatGpuType(gpuType),
|
||||||
},
|
},
|
||||||
...(gpuCount > 0
|
...(gpuCount > 0 ? [{ name: 'GPU Devices (i915/xe)', value: String(gpuCount) }] : []),
|
||||||
? [{ name: 'GPU Devices (i915/xe)', value: String(gpuCount) }]
|
|
||||||
: []),
|
|
||||||
...Object.entries(capacityResources).map(([key, cap]) => {
|
...Object.entries(capacityResources).map(([key, cap]) => {
|
||||||
const alloc = parseInt(allocatableResources[key] ?? '0', 10);
|
|
||||||
const total = parseInt(cap, 10);
|
const total = parseInt(cap, 10);
|
||||||
return {
|
return {
|
||||||
name: `${formatGpuResourceName(key)} (capacity)`,
|
name: `${formatGpuResourceName(key)} (capacity)`,
|
||||||
value: String(total),
|
value: String(total),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
...Object.entries(allocatableResources).map(([key, alloc]) => {
|
...Object.entries(allocatableResources).map(([key, value]) => {
|
||||||
return {
|
return {
|
||||||
name: `${formatGpuResourceName(key)} (allocatable)`,
|
name: `${formatGpuResourceName(key)} (allocatable)`,
|
||||||
value: alloc ?? '0',
|
value: value ?? '0',
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@@ -200,7 +190,14 @@ export default function NodesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SectionHeader title="Intel GPU — Nodes" />
|
<SectionHeader title="Intel GPU — Nodes" />
|
||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
@@ -256,28 +253,28 @@ export default function NodesPage() {
|
|||||||
<SectionBox title="GPU Node Summary">
|
<SectionBox title="GPU Node Summary">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Node', getter: (d) => d.node.metadata.name },
|
{ label: 'Node', getter: d => d.node.metadata.name },
|
||||||
{
|
{
|
||||||
label: 'Ready',
|
label: 'Ready',
|
||||||
getter: (d) => (
|
getter: d => (
|
||||||
<StatusLabel status={d.ready ? 'success' : 'error'}>
|
<StatusLabel status={d.ready ? 'success' : 'error'}>
|
||||||
{d.ready ? 'Ready' : 'Not Ready'}
|
{d.ready ? 'Ready' : 'Not Ready'}
|
||||||
</StatusLabel>
|
</StatusLabel>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ label: 'GPU Type', getter: (d) => formatGpuType(d.gpuType) },
|
{ label: 'GPU Type', getter: d => formatGpuType(d.gpuType) },
|
||||||
{ label: 'GPU Devices', getter: (d) => String(d.gpuCount || '—') },
|
{ label: 'GPU Devices', getter: d => String(d.gpuCount || '—') },
|
||||||
{
|
{
|
||||||
label: 'Allocation',
|
label: 'Allocation',
|
||||||
getter: (d) => (
|
getter: d => (
|
||||||
<GpuAllocationBar
|
<GpuAllocationBar
|
||||||
used={d.podsOnNode.length}
|
used={d.podsOnNode.length}
|
||||||
allocatable={d.totalAllocatable || d.gpuCount}
|
allocatable={d.totalAllocatable || d.gpuCount}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ label: 'GPU Pods', getter: (d) => String(d.podsOnNode.length) },
|
{ label: 'GPU Pods', getter: d => String(d.podsOnNode.length) },
|
||||||
{ label: 'Age', getter: (d) => formatAge(d.node.metadata.creationTimestamp) },
|
{ label: 'Age', getter: d => formatAge(d.node.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={tableData}
|
data={tableData}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import React from 'react';
|
|||||||
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||||
import {
|
import {
|
||||||
formatAge,
|
formatAge,
|
||||||
formatGpuType,
|
|
||||||
getNodeGpuCount,
|
getNodeGpuCount,
|
||||||
getNodeGpuType,
|
getNodeGpuType,
|
||||||
getPodGpuRequests,
|
getPodGpuRequests,
|
||||||
@@ -42,7 +41,8 @@ function gpuTypeChartData(
|
|||||||
): Array<{ name: string; value: number; fill: string }> {
|
): Array<{ name: string; value: number; fill: string }> {
|
||||||
const data = [];
|
const data = [];
|
||||||
if (discreteCount > 0) data.push({ name: 'Discrete', value: discreteCount, fill: '#0071c5' });
|
if (discreteCount > 0) data.push({ name: 'Discrete', value: discreteCount, fill: '#0071c5' });
|
||||||
if (integratedCount > 0) data.push({ name: 'Integrated', value: integratedCount, fill: '#60a4dc' });
|
if (integratedCount > 0)
|
||||||
|
data.push({ name: 'Integrated', value: integratedCount, fill: '#60a4dc' });
|
||||||
if (unknownCount > 0) data.push({ name: 'Unknown', value: unknownCount, fill: '#9e9e9e' });
|
if (unknownCount > 0) data.push({ name: 'Unknown', value: unknownCount, fill: '#9e9e9e' });
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -113,9 +113,7 @@ export default function OverviewPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gpuUtilizationPct =
|
const gpuUtilizationPct =
|
||||||
totalCapacityGpus > 0
|
totalCapacityGpus > 0 ? Math.round((totalAllocatedGpus / totalCapacityGpus) * 100) : 0;
|
||||||
? Math.round((totalAllocatedGpus / totalCapacityGpus) * 100)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const chartData = gpuTypeChartData(discreteCount, integratedCount, unknownCount);
|
const chartData = gpuTypeChartData(discreteCount, integratedCount, unknownCount);
|
||||||
const totalGpuNodes = gpuNodes.length;
|
const totalGpuNodes = gpuNodes.length;
|
||||||
@@ -133,7 +131,14 @@ export default function OverviewPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SectionHeader title="Intel GPU — Overview" />
|
<SectionHeader title="Intel GPU — Overview" />
|
||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
@@ -218,26 +223,25 @@ export default function OverviewPage() {
|
|||||||
<SectionBox title="Device Plugin Status">
|
<SectionBox title="Device Plugin Status">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
{ label: 'Name', getter: p => p.metadata.name },
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
getter: (p) => (
|
getter: p => (
|
||||||
<StatusLabel status={pluginStatusToStatus(p)}>
|
<StatusLabel status={pluginStatusToStatus(p)}>{pluginStatusText(p)}</StatusLabel>
|
||||||
{pluginStatusText(p)}
|
|
||||||
</StatusLabel>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Monitoring',
|
label: 'Monitoring',
|
||||||
getter: (p) => p.spec.enableMonitoring ? (
|
getter: p =>
|
||||||
<StatusLabel status="success">Enabled</StatusLabel>
|
p.spec.enableMonitoring ? (
|
||||||
) : (
|
<StatusLabel status="success">Enabled</StatusLabel>
|
||||||
<StatusLabel status="warning">Disabled</StatusLabel>
|
) : (
|
||||||
),
|
<StatusLabel status="warning">Disabled</StatusLabel>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{ label: 'Shared/Node', getter: (p) => String(p.spec.sharedDevNum ?? 1) },
|
{ label: 'Shared/Node', getter: p => String(p.spec.sharedDevNum ?? 1) },
|
||||||
{ label: 'Policy', getter: (p) => p.spec.preferredAllocationPolicy ?? '—' },
|
{ label: 'Policy', getter: p => p.spec.preferredAllocationPolicy ?? '—' },
|
||||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={devicePlugins}
|
data={devicePlugins}
|
||||||
/>
|
/>
|
||||||
@@ -249,18 +253,18 @@ export default function OverviewPage() {
|
|||||||
<SectionBox title="Plugin Daemon Pods">
|
<SectionBox title="Plugin Daemon Pods">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
{ label: 'Name', getter: p => p.metadata.name },
|
||||||
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
|
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
|
||||||
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
|
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
|
||||||
{
|
{
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
getter: (p) => (
|
getter: p => (
|
||||||
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
|
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
|
||||||
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
|
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
|
||||||
</StatusLabel>
|
</StatusLabel>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={pluginPods}
|
data={pluginPods}
|
||||||
/>
|
/>
|
||||||
@@ -271,7 +275,13 @@ export default function OverviewPage() {
|
|||||||
<SectionBox title="GPU Nodes">
|
<SectionBox title="GPU Nodes">
|
||||||
{totalGpuNodes > 0 && chartData.length > 0 && (
|
{totalGpuNodes > 0 && chartData.length > 0 && (
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<div style={{ marginBottom: '8px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: 'var(--mui-palette-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
GPU Type Distribution
|
GPU Type Distribution
|
||||||
</div>
|
</div>
|
||||||
<PercentageBar data={chartData} total={totalGpuNodes} />
|
<PercentageBar data={chartData} total={totalGpuNodes} />
|
||||||
@@ -288,9 +298,15 @@ export default function OverviewPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ name: 'Ready Nodes', value: String(readyNodeCount) },
|
{ name: 'Ready Nodes', value: String(readyNodeCount) },
|
||||||
...(discreteCount > 0 ? [{ name: 'Discrete GPU Nodes', value: String(discreteCount) }] : []),
|
...(discreteCount > 0
|
||||||
...(integratedCount > 0 ? [{ name: 'Integrated GPU Nodes', value: String(integratedCount) }] : []),
|
? [{ name: 'Discrete GPU Nodes', value: String(discreteCount) }]
|
||||||
...(totalGpuCount > 0 ? [{ name: 'Total GPU Devices', value: String(totalGpuCount) }] : []),
|
: []),
|
||||||
|
...(integratedCount > 0
|
||||||
|
? [{ name: 'Integrated GPU Nodes', value: String(integratedCount) }]
|
||||||
|
: []),
|
||||||
|
...(totalGpuCount > 0
|
||||||
|
? [{ name: 'Total GPU Devices', value: String(totalGpuCount) }]
|
||||||
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</SectionBox>
|
</SectionBox>
|
||||||
@@ -299,13 +315,23 @@ export default function OverviewPage() {
|
|||||||
{totalCapacityGpus > 0 && (
|
{totalCapacityGpus > 0 && (
|
||||||
<SectionBox title="GPU Allocation">
|
<SectionBox title="GPU Allocation">
|
||||||
<div style={{ marginBottom: '16px' }}>
|
<div style={{ marginBottom: '16px' }}>
|
||||||
<div style={{ marginBottom: '8px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '8px',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: 'var(--mui-palette-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
GPU Utilization ({gpuUtilizationPct}%)
|
GPU Utilization ({gpuUtilizationPct}%)
|
||||||
</div>
|
</div>
|
||||||
<PercentageBar
|
<PercentageBar
|
||||||
data={[
|
data={[
|
||||||
{ name: 'In Use', value: totalAllocatedGpus, fill: '#0071c5' },
|
{ name: 'In Use', value: totalAllocatedGpus, fill: '#0071c5' },
|
||||||
{ name: 'Available', value: totalAllocatableGpus - totalAllocatedGpus, fill: '#e0e0e0' },
|
{
|
||||||
|
name: 'Available',
|
||||||
|
value: totalAllocatableGpus - totalAllocatedGpus,
|
||||||
|
fill: '#e0e0e0',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
total={totalAllocatableGpus}
|
total={totalAllocatableGpus}
|
||||||
/>
|
/>
|
||||||
@@ -336,13 +362,28 @@ export default function OverviewPage() {
|
|||||||
rows={[
|
rows={[
|
||||||
{ name: 'Total GPU Pods', value: String(gpuPods.length) },
|
{ name: 'Total GPU Pods', value: String(gpuPods.length) },
|
||||||
...(podPhaseCounts.Running > 0
|
...(podPhaseCounts.Running > 0
|
||||||
? [{ name: 'Running', value: <StatusLabel status="success">{podPhaseCounts.Running}</StatusLabel> }]
|
? [
|
||||||
|
{
|
||||||
|
name: 'Running',
|
||||||
|
value: <StatusLabel status="success">{podPhaseCounts.Running}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
...(podPhaseCounts.Pending > 0
|
...(podPhaseCounts.Pending > 0
|
||||||
? [{ name: 'Pending', value: <StatusLabel status="warning">{podPhaseCounts.Pending}</StatusLabel> }]
|
? [
|
||||||
|
{
|
||||||
|
name: 'Pending',
|
||||||
|
value: <StatusLabel status="warning">{podPhaseCounts.Pending}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
...(podPhaseCounts.Failed > 0
|
...(podPhaseCounts.Failed > 0
|
||||||
? [{ name: 'Failed', value: <StatusLabel status="error">{podPhaseCounts.Failed}</StatusLabel> }]
|
? [
|
||||||
|
{
|
||||||
|
name: 'Failed',
|
||||||
|
value: <StatusLabel status="error">{podPhaseCounts.Failed}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -353,12 +394,12 @@ export default function OverviewPage() {
|
|||||||
<SectionBox title="Active GPU Pods">
|
<SectionBox title="Active GPU Pods">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
{ label: 'Name', getter: p => p.metadata.name },
|
||||||
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
|
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
|
||||||
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
|
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
|
||||||
{
|
{
|
||||||
label: 'GPU Request',
|
label: 'GPU Request',
|
||||||
getter: (p) => {
|
getter: p => {
|
||||||
const reqs = getPodGpuRequests(p);
|
const reqs = getPodGpuRequests(p);
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
for (const [key, val] of Object.entries(reqs)) {
|
for (const [key, val] of Object.entries(reqs)) {
|
||||||
@@ -368,7 +409,7 @@ export default function OverviewPage() {
|
|||||||
return parts.join(', ') || '—';
|
return parts.join(', ') || '—';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={gpuPods.filter(p => p.status?.phase === 'Running').slice(0, 10)}
|
data={gpuPods.filter(p => p.status?.phase === 'Running').slice(0, 10)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ interface PodDetailSectionProps {
|
|||||||
export default function PodDetailSection({ resource }: PodDetailSectionProps) {
|
export default function PodDetailSection({ resource }: PodDetailSectionProps) {
|
||||||
// Extract raw Kubernetes JSON
|
// Extract raw Kubernetes JSON
|
||||||
const rawPod =
|
const rawPod =
|
||||||
resource.jsonData && typeof resource.jsonData === 'object'
|
resource.jsonData && typeof resource.jsonData === 'object' ? resource.jsonData : resource;
|
||||||
? resource.jsonData
|
|
||||||
: resource;
|
|
||||||
|
|
||||||
// Only render for pods that request Intel GPU resources
|
// Only render for pods that request Intel GPU resources
|
||||||
if (!isGpuRequestingPod(rawPod)) return null;
|
if (!isGpuRequestingPod(rawPod)) return null;
|
||||||
@@ -98,9 +96,7 @@ export default function PodDetailSection({ resource }: PodDetailSectionProps) {
|
|||||||
rows={[
|
rows={[
|
||||||
{
|
{
|
||||||
name: 'Phase',
|
name: 'Phase',
|
||||||
value: (
|
value: <StatusLabel status={phaseStatus}>{phase ?? 'Unknown'}</StatusLabel>,
|
||||||
<StatusLabel status={phaseStatus}>{phase ?? 'Unknown'}</StatusLabel>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Scheduled Node',
|
name: 'Scheduled Node',
|
||||||
|
|||||||
+55
-30
@@ -17,11 +17,10 @@ import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
|||||||
import {
|
import {
|
||||||
formatAge,
|
formatAge,
|
||||||
formatGpuResourceName,
|
formatGpuResourceName,
|
||||||
IntelGpuPod,
|
|
||||||
INTEL_GPU_RESOURCE_PREFIX,
|
|
||||||
isPodReady,
|
|
||||||
getPodGpuRequests,
|
getPodGpuRequests,
|
||||||
getPodRestarts,
|
getPodRestarts,
|
||||||
|
INTEL_GPU_RESOURCE_PREFIX,
|
||||||
|
IntelGpuPod,
|
||||||
} from '../api/k8s';
|
} from '../api/k8s';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -30,11 +29,16 @@ import {
|
|||||||
|
|
||||||
function phaseToStatus(phase: string | undefined): 'success' | 'warning' | 'error' {
|
function phaseToStatus(phase: string | undefined): 'success' | 'warning' | 'error' {
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 'Running': return 'success';
|
case 'Running':
|
||||||
case 'Succeeded': return 'success';
|
return 'success';
|
||||||
case 'Pending': return 'warning';
|
case 'Succeeded':
|
||||||
case 'Failed': return 'error';
|
return 'success';
|
||||||
default: return 'warning';
|
case 'Pending':
|
||||||
|
return 'warning';
|
||||||
|
case 'Failed':
|
||||||
|
return 'error';
|
||||||
|
default:
|
||||||
|
return 'warning';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,13 +102,17 @@ export default function PodsPage() {
|
|||||||
const running = gpuPods.filter(p => p.status?.phase === 'Running');
|
const running = gpuPods.filter(p => p.status?.phase === 'Running');
|
||||||
const pending = gpuPods.filter(p => p.status?.phase === 'Pending');
|
const pending = gpuPods.filter(p => p.status?.phase === 'Pending');
|
||||||
const failed = gpuPods.filter(p => p.status?.phase === 'Failed');
|
const failed = gpuPods.filter(p => p.status?.phase === 'Failed');
|
||||||
const other = gpuPods.filter(
|
|
||||||
p => !['Running', 'Pending', 'Failed'].includes(p.status?.phase ?? '')
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SectionHeader title="Intel GPU — Pods" />
|
<SectionHeader title="Intel GPU — Pods" />
|
||||||
<button
|
<button
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
@@ -161,13 +169,28 @@ export default function PodsPage() {
|
|||||||
rows={[
|
rows={[
|
||||||
{ name: 'Total GPU Pods', value: String(gpuPods.length) },
|
{ name: 'Total GPU Pods', value: String(gpuPods.length) },
|
||||||
...(running.length > 0
|
...(running.length > 0
|
||||||
? [{ name: 'Running', value: <StatusLabel status="success">{running.length}</StatusLabel> }]
|
? [
|
||||||
|
{
|
||||||
|
name: 'Running',
|
||||||
|
value: <StatusLabel status="success">{running.length}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
...(pending.length > 0
|
...(pending.length > 0
|
||||||
? [{ name: 'Pending', value: <StatusLabel status="warning">{pending.length}</StatusLabel> }]
|
? [
|
||||||
|
{
|
||||||
|
name: 'Pending',
|
||||||
|
value: <StatusLabel status="warning">{pending.length}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
...(failed.length > 0
|
...(failed.length > 0
|
||||||
? [{ name: 'Failed', value: <StatusLabel status="error">{failed.length}</StatusLabel> }]
|
? [
|
||||||
|
{
|
||||||
|
name: 'Failed',
|
||||||
|
value: <StatusLabel status="error">{failed.length}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -179,12 +202,12 @@ export default function PodsPage() {
|
|||||||
<SectionBox title="All GPU Pods">
|
<SectionBox title="All GPU Pods">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
{ label: 'Name', getter: p => p.metadata.name },
|
||||||
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
|
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
|
||||||
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
|
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
|
||||||
{
|
{
|
||||||
label: 'Phase',
|
label: 'Phase',
|
||||||
getter: (p) => (
|
getter: p => (
|
||||||
<StatusLabel status={phaseToStatus(p.status?.phase)}>
|
<StatusLabel status={phaseToStatus(p.status?.phase)}>
|
||||||
{p.status?.phase ?? 'Unknown'}
|
{p.status?.phase ?? 'Unknown'}
|
||||||
</StatusLabel>
|
</StatusLabel>
|
||||||
@@ -192,11 +215,11 @@ export default function PodsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'GPU Resources',
|
label: 'GPU Resources',
|
||||||
getter: (p) => <GpuContainerList pod={p} />,
|
getter: p => <GpuContainerList pod={p} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Restarts',
|
label: 'Restarts',
|
||||||
getter: (p) => {
|
getter: p => {
|
||||||
const restarts = getPodRestarts(p);
|
const restarts = getPodRestarts(p);
|
||||||
return restarts > 0 ? (
|
return restarts > 0 ? (
|
||||||
<StatusLabel status="warning">{restarts}</StatusLabel>
|
<StatusLabel status="warning">{restarts}</StatusLabel>
|
||||||
@@ -205,7 +228,7 @@ export default function PodsPage() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={gpuPods}
|
data={gpuPods}
|
||||||
/>
|
/>
|
||||||
@@ -217,25 +240,27 @@ export default function PodsPage() {
|
|||||||
<SectionBox title="Attention: Pending GPU Pods">
|
<SectionBox title="Attention: Pending GPU Pods">
|
||||||
<SimpleTable
|
<SimpleTable
|
||||||
columns={[
|
columns={[
|
||||||
{ label: 'Name', getter: (p) => p.metadata.name },
|
{ label: 'Name', getter: p => p.metadata.name },
|
||||||
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
|
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
|
||||||
{
|
{
|
||||||
label: 'GPU Resources',
|
label: 'GPU Resources',
|
||||||
getter: (p) => {
|
getter: p => {
|
||||||
const reqs = getPodGpuRequests(p);
|
const reqs = getPodGpuRequests(p);
|
||||||
return Object.entries(reqs)
|
return (
|
||||||
.map(([k, v]) => `${formatGpuResourceName(k)}: ${v}`)
|
Object.entries(reqs)
|
||||||
.join(', ') || '—';
|
.map(([k, v]) => `${formatGpuResourceName(k)}: ${v}`)
|
||||||
|
.join(', ') || '—'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Waiting Reason',
|
label: 'Waiting Reason',
|
||||||
getter: (p) => {
|
getter: p => {
|
||||||
const reason = p.status?.containerStatuses?.[0]?.state?.waiting?.reason;
|
const reason = p.status?.containerStatuses?.[0]?.state?.waiting?.reason;
|
||||||
return reason ?? '—';
|
return reason ?? '—';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
|
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
|
||||||
]}
|
]}
|
||||||
data={pending}
|
data={pending}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,12 +11,7 @@
|
|||||||
|
|
||||||
import { StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
import { StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { formatGpuType, getNodeGpuCount, getNodeGpuType, isIntelGpuNode } from '../../api/k8s';
|
||||||
formatGpuType,
|
|
||||||
getNodeGpuCount,
|
|
||||||
getNodeGpuType,
|
|
||||||
isIntelGpuNode,
|
|
||||||
} from '../../api/k8s';
|
|
||||||
|
|
||||||
/** Build GPU columns to append to the native Nodes table. */
|
/** Build GPU columns to append to the native Nodes table. */
|
||||||
export function buildNodeGpuColumns() {
|
export function buildNodeGpuColumns() {
|
||||||
@@ -33,11 +28,7 @@ export function buildNodeGpuColumns() {
|
|||||||
if (!isIntelGpuNode(raw)) return '—';
|
if (!isIntelGpuNode(raw)) return '—';
|
||||||
const node = raw as Parameters<typeof getNodeGpuType>[0];
|
const node = raw as Parameters<typeof getNodeGpuType>[0];
|
||||||
const type = getNodeGpuType(node);
|
const type = getNodeGpuType(node);
|
||||||
return (
|
return <StatusLabel status="success">{formatGpuType(type)}</StatusLabel>;
|
||||||
<StatusLabel status="success">
|
|
||||||
{formatGpuType(type)}
|
|
||||||
</StatusLabel>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+33
-34
@@ -34,49 +34,49 @@ import PodsPage from './components/PodsPage';
|
|||||||
|
|
||||||
registerSidebarEntry({
|
registerSidebarEntry({
|
||||||
parent: null,
|
parent: null,
|
||||||
name: 'intel-gpu',
|
name: 'headlamp-intel-gpu',
|
||||||
label: 'intel-gpu',
|
label: 'headlamp-intel-gpu',
|
||||||
url: '/intel-gpu',
|
url: '/headlamp-intel-gpu',
|
||||||
icon: 'mdi:gpu',
|
icon: 'mdi:gpu',
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSidebarEntry({
|
registerSidebarEntry({
|
||||||
parent: 'intel-gpu',
|
parent: 'headlamp-intel-gpu',
|
||||||
name: 'intel-gpu-overview',
|
name: 'headlamp-intel-gpu-overview',
|
||||||
label: 'Overview',
|
label: 'Overview',
|
||||||
url: '/intel-gpu',
|
url: '/headlamp-intel-gpu',
|
||||||
icon: 'mdi:view-dashboard',
|
icon: 'mdi:view-dashboard',
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSidebarEntry({
|
registerSidebarEntry({
|
||||||
parent: 'intel-gpu',
|
parent: 'headlamp-intel-gpu',
|
||||||
name: 'intel-gpu-device-plugins',
|
name: 'headlamp-intel-gpu-device-plugins',
|
||||||
label: 'Device Plugins',
|
label: 'Device Plugins',
|
||||||
url: '/intel-gpu/device-plugins',
|
url: '/headlamp-intel-gpu/device-plugins',
|
||||||
icon: 'mdi:chip',
|
icon: 'mdi:chip',
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSidebarEntry({
|
registerSidebarEntry({
|
||||||
parent: 'intel-gpu',
|
parent: 'headlamp-intel-gpu',
|
||||||
name: 'intel-gpu-nodes',
|
name: 'headlamp-intel-gpu-nodes',
|
||||||
label: 'GPU Nodes',
|
label: 'GPU Nodes',
|
||||||
url: '/intel-gpu/nodes',
|
url: '/headlamp-intel-gpu/nodes',
|
||||||
icon: 'mdi:server',
|
icon: 'mdi:server',
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSidebarEntry({
|
registerSidebarEntry({
|
||||||
parent: 'intel-gpu',
|
parent: 'headlamp-intel-gpu',
|
||||||
name: 'intel-gpu-pods',
|
name: 'headlamp-intel-gpu-pods',
|
||||||
label: 'GPU Pods',
|
label: 'GPU Pods',
|
||||||
url: '/intel-gpu/pods',
|
url: '/headlamp-intel-gpu/pods',
|
||||||
icon: 'mdi:cube-outline',
|
icon: 'mdi:cube-outline',
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSidebarEntry({
|
registerSidebarEntry({
|
||||||
parent: 'intel-gpu',
|
parent: 'headlamp-intel-gpu',
|
||||||
name: 'intel-gpu-metrics',
|
name: 'headlamp-intel-gpu-metrics',
|
||||||
label: 'Metrics',
|
label: 'Metrics',
|
||||||
url: '/intel-gpu/metrics',
|
url: '/headlamp-intel-gpu/metrics',
|
||||||
icon: 'mdi:chart-line',
|
icon: 'mdi:chart-line',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,9 +85,9 @@ registerSidebarEntry({
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/intel-gpu',
|
path: '/headlamp-intel-gpu',
|
||||||
sidebar: 'intel-gpu-overview',
|
sidebar: 'headlamp-intel-gpu-overview',
|
||||||
name: 'intel-gpu-overview',
|
name: 'headlamp-intel-gpu-overview',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
<IntelGpuDataProvider>
|
<IntelGpuDataProvider>
|
||||||
@@ -97,9 +97,9 @@ registerRoute({
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/intel-gpu/device-plugins',
|
path: '/headlamp-intel-gpu/device-plugins',
|
||||||
sidebar: 'intel-gpu-device-plugins',
|
sidebar: 'headlamp-intel-gpu-device-plugins',
|
||||||
name: 'intel-gpu-device-plugins',
|
name: 'headlamp-intel-gpu-device-plugins',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
<IntelGpuDataProvider>
|
<IntelGpuDataProvider>
|
||||||
@@ -109,9 +109,9 @@ registerRoute({
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/intel-gpu/nodes',
|
path: '/headlamp-intel-gpu/nodes',
|
||||||
sidebar: 'intel-gpu-nodes',
|
sidebar: 'headlamp-intel-gpu-nodes',
|
||||||
name: 'intel-gpu-nodes',
|
name: 'headlamp-intel-gpu-nodes',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
<IntelGpuDataProvider>
|
<IntelGpuDataProvider>
|
||||||
@@ -121,9 +121,9 @@ registerRoute({
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/intel-gpu/pods',
|
path: '/headlamp-intel-gpu/pods',
|
||||||
sidebar: 'intel-gpu-pods',
|
sidebar: 'headlamp-intel-gpu-pods',
|
||||||
name: 'intel-gpu-pods',
|
name: 'headlamp-intel-gpu-pods',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
<IntelGpuDataProvider>
|
<IntelGpuDataProvider>
|
||||||
@@ -133,9 +133,9 @@ registerRoute({
|
|||||||
});
|
});
|
||||||
|
|
||||||
registerRoute({
|
registerRoute({
|
||||||
path: '/intel-gpu/metrics',
|
path: '/headlamp-intel-gpu/metrics',
|
||||||
sidebar: 'intel-gpu-metrics',
|
sidebar: 'headlamp-intel-gpu-metrics',
|
||||||
name: 'intel-gpu-metrics',
|
name: 'headlamp-intel-gpu-metrics',
|
||||||
exact: true,
|
exact: true,
|
||||||
component: () => (
|
component: () => (
|
||||||
<IntelGpuDataProvider>
|
<IntelGpuDataProvider>
|
||||||
@@ -180,4 +180,3 @@ registerResourceTableColumnsProcessor(({ id, columns }) => {
|
|||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user