Plugin Development
Purpose: For contributors, shows how to create a Headlamp plugin from scratch with scaffold, develop, test, and package steps. Covers both the standalone headlamp-plugin CLI and the openCenter monorepo workflow.
Outcome
By the end of this tutorial you will have a working Headlamp plugin that adds UI functionality, built and tested locally. Choose the path that fits your situation:
- Standalone plugin — fastest way to get started. Uses the official
headlamp-pluginCLI to scaffold, build, and package. - Monorepo plugin — for openCenter branding and multi-plugin projects. Uses the
headlamp-branding-pluginmonorepo with shared tooling.
Prerequisites
- Node.js >= 20.18.1 (download)
- npm (comes with Node.js) or pnpm >= 9.0.0 (for monorepo path)
- A running Headlamp instance — desktop app or development setup
- Familiarity with React and TypeScript
Path A: Standalone Plugin (headlamp-plugin CLI)
This is the recommended starting point. The headlamp-plugin CLI scaffolds a complete plugin project with build tooling, linting, and TypeScript configuration.
Step 1: Scaffold the Plugin
Run this in your projects directory (not inside Headlamp's plugin installation directory):
npx --yes @kinvolk/headlamp-plugin create my-first-plugin
cd my-first-plugin
npm install
Generated structure:
my-first-plugin/
├── src/
│ └── index.tsx # Main plugin entry point
├── package.json # Plugin metadata and dependencies
├── tsconfig.json # TypeScript configuration
└── README.md
Step 2: Examine the Default Code
Open src/index.tsx:
import { registerAppBarAction } from '@kinvolk/headlamp-plugin/lib';
registerAppBarAction(<span>Hello</span>);
This registers a component in the top-right app bar. The plugin registry (covered below) provides many more registration points.
Step 3: Start Development Mode
npm run start
This watches for file changes and automatically rebuilds the plugin. Open Headlamp (desktop app or dev server) — you should see "Hello" in the top navigation bar.
Step 4: Make a Change
Replace src/index.tsx with something interactive:
import { registerAppBarAction } from '@kinvolk/headlamp-plugin/lib';
import { Button } from '@mui/material';
function HelloButton() {
const handleClick = () => {
alert('Hello from your Headlamp plugin!');
};
return (
<Button variant="outlined" size="small" onClick={handleClick} sx={{ mx: 2 }}>
Hello Headlamp!
</Button>
);
}
registerAppBarAction(<HelloButton />);
Save the file. Headlamp reloads automatically.
Step 5: Code Quality and Testing
The scaffolded project includes quality tools:
npm run format # Format code
npm run lint # Check for linting issues
npm run lint-fix # Auto-fix linting issues
npm run tsc # Type checking
npm run test # Run tests
Step 6: Build and Package for Production
npm run build
npm run package
This creates a tarball (e.g., my-first-plugin-0.1.0.tar.gz) that can be extracted into the Headlamp plugins directory for deployment.
Path B: Monorepo Plugin (openCenter)
Use this path when building plugins for the openCenter Headlamp deployment, where plugins share tooling and are managed together in the headlamp-branding-plugin monorepo.
Step 1: Scaffold the Plugin
Create the plugin directory inside the monorepo:
mkdir -p plugins/my-plugin/src
mkdir -p plugins/my-plugin/assets
mkdir -p plugins/my-plugin/__tests__
Create plugins/my-plugin/package.json:
{
"name": "@opencenter/headlamp-plugin-my-plugin",
"version": "0.1.0",
"description": "My custom Headlamp plugin",
"main": "dist/main.js",
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"test": "jest",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext .ts,.tsx",
"format": "prettier --write \"src/**/*.{ts,tsx}\""
},
"peerDependencies": {
"@kinvolk/headlamp-plugin": ">=0.7.0 <1.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.7.3",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"copy-webpack-plugin": "^14.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.2.5",
"@types/react": "^18.3.18",
"@types/jest": "^29.5.14"
},
"engines": {
"node": ">=20.0.0"
}
}
Step 2: Add Webpack Configuration
Create plugins/my-plugin/webpack.config.js:
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = (env, argv) => ({
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
library: { type: 'commonjs2' },
},
externals: {
react: 'react',
'react-dom': 'react-dom',
'@kinvolk/headlamp-plugin/lib': '@kinvolk/headlamp-plugin/lib',
},
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
{ test: /\.(svg|png)$/, type: 'asset/resource' },
],
},
plugins: [
new CopyPlugin({
patterns: [
{ from: 'package.json', to: 'package.json' },
{ from: 'assets', to: 'assets', noErrorOnMissing: true },
],
}),
],
resolve: { extensions: ['.tsx', '.ts', '.js'] },
devtool: argv.mode === 'development' ? 'source-map' : false,
});
The three externals entries are critical. React and the Headlamp plugin library are provided by the host at runtime — bundling them causes version conflicts.
Step 3: Add TypeScript Configuration
Create plugins/my-plugin/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "__tests__"]
}
Step 4: Write the Plugin Entry Point
Create plugins/my-plugin/src/index.tsx:
import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib';
import MyPage from './components/MyPage';
// Add a sidebar entry
registerSidebarEntry({
parent: null,
name: 'my-plugin',
label: 'My Plugin',
url: '/my-plugin',
});
// Register the route
registerRoute({
path: '/my-plugin',
component: () => <MyPage />,
name: 'my-plugin',
sidebar: 'My Plugin',
});
Create plugins/my-plugin/src/components/MyPage.tsx:
import React from 'react';
const MyPage: React.FC = () => (
<div style={{ padding: '24px' }}>
<h1>My Plugin Page</h1>
<p>This page was added by the my-plugin Headlamp plugin.</p>
</div>
);
export default MyPage;
Step 5: Install Dependencies and Build
From the workspace root:
pnpm install
pnpm --filter @opencenter/headlamp-plugin-my-plugin run build
Confirm the output:
ls plugins/my-plugin/dist/
# Expected: main.js package.json assets/
Step 6: Test Locally with Podman
Run Headlamp with the plugin mounted:
podman run -d \
--name headlamp-dev \
-p 4466:4466 \
-v $(pwd)/plugins/my-plugin/dist:/headlamp/plugins/my-plugin \
-v ~/.kube/config:/root/.kube/config:ro \
ghcr.io/headlamp-k8s/headlamp:latest
Open http://localhost:4466. You should see "My Plugin" in the sidebar. Click it to load the custom page.
For a faster iteration loop, run webpack in watch mode in one terminal and restart the container after changes:
# Terminal 1
pnpm --filter @opencenter/headlamp-plugin-my-plugin run dev
# Terminal 2 (after saving changes)
podman restart headlamp-dev
Clean up when done:
podman stop headlamp-dev && podman rm headlamp-dev
Step 7: Add Tests
Create plugins/my-plugin/jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src', '<rootDir>/__tests__'],
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
};
Create plugins/my-plugin/src/__tests__/MyPage.test.tsx:
import React from 'react';
import { render, screen } from '@testing-library/react';
import MyPage from '../components/MyPage';
test('renders the plugin page heading', () => {
render(<MyPage />);
expect(screen.getByText('My Plugin Page')).toBeInTheDocument();
});
Run tests:
pnpm --filter @opencenter/headlamp-plugin-my-plugin test
Plugin Registry API
The plugin registry is the central system for extending Headlamp. All registration functions are imported from @kinvolk/headlamp-plugin/lib.
import {
registerAppBarAction,
registerAppLogo,
registerRoute,
registerSidebarEntry,
registerSidebarEntryFilter,
registerDetailsViewSection,
registerDetailsViewSectionsProcessor,
registerDetailsViewHeaderAction,
registerClusterChooser,
registerResourceTableColumnsProcessor,
registerHeadlampEventCallback,
registerPluginSettings,
registerAppTheme,
registerRouteFilter,
registerUIPanel,
registerProjectDetailsTab,
registerProjectOverviewSection,
} from '@kinvolk/headlamp-plugin/lib';
Registration Functions
| Function | What It Does |
|---|---|
registerAppBarAction | Add a component to the top-right app bar. |
registerAppLogo | Replace the logo in the top-left corner. |
registerRoute | Show a component at a given URL path. |
registerRouteFilter | Filter or modify registered routes. |
registerSidebarEntry | Add items to the left sidebar navigation. |
registerSidebarEntryFilter | Remove or modify sidebar items. |
registerDetailsViewSection | Add a component to the bottom of a resource details view. |
registerDetailsViewSectionsProcessor | Add, remove, update, or reorder sections in a details view. |
registerDetailsViewHeaderAction | Add a component to the top-right of a details view. |
registerClusterChooser | Replace the cluster chooser button in the app bar. |
registerResourceTableColumnsProcessor | Add, remove, update, or reorder columns in resource tables. |
registerHeadlampEventCallback | React to Headlamp lifecycle events (e.g., resource changes). |
registerPluginSettings | Define user-configurable settings for the plugin. |
registerAppTheme | Add a custom theme (light or dark base). Appears in General Settings. |
registerUIPanel | Register a side panel (top, left, right, or bottom). |
registerProjectDetailsTab | Add custom tabs to the project details view. |
registerProjectOverviewSection | Add custom sections to the project overview page. |
For desktop app deployments, Headlamp.setAppMenu adds custom menus and Headlamp.setCluster configures clusters dynamically (without a config file).
Plugin Lib Modules
The @kinvolk/headlamp-plugin/lib package exposes several modules beyond the registration functions:
| Module | Purpose |
|---|---|
K8s | Kubernetes API access — resource classes, hooks (useList, useGet), and CRUD operations. |
CommonComponents | Reusable React components used throughout the Headlamp UI (SectionBox, etc.). |
Notification | Create and dispatch notifications to the Headlamp notification system. |
Router | Get or generate routes programmatically. |
Kubernetes API Access
Access Kubernetes resources using the K8s module:
import { K8s } from '@kinvolk/headlamp-plugin/lib';
function PodList() {
const [pods, error] = K8s.ResourceClasses.Pod.useList();
if (error) return <div>Error loading pods</div>;
if (!pods) return <div>Loading...</div>;
return (
<div>
<h3>Pods ({pods.length})</h3>
{pods.map(pod => (
<div key={pod.metadata.uid}>{pod.metadata.name}</div>
))}
</div>
);
}
Shared Dependencies
Headlamp provides common libraries at runtime. Plugins can import these normally in code, but they are not bundled into the plugin output — Headlamp injects them via the pluginLib global. Do not add these to your plugin's dependencies in package.json:
- React and React DOM
- React Router
- Redux and React-Redux
- Material-UI (
@mui/material,@mui/lab) - Lodash
- Notistack (snackbar notifications)
- Monaco Editor
@iconify/react- Recharts
If your plugin imports a library that Headlamp already provides, the build process replaces it with the host version. Version mismatches between your plugin and Headlamp's bundled version can cause runtime errors.
Troubleshooting
Plugin Not Loading
- Confirm Headlamp is running and accessible.
- Check the browser console for JavaScript errors.
- Verify
package.jsonhas correct metadata (name, version, main). - For standalone plugins, confirm
npm run startis running in the plugin directory.
Changes Not Reflecting
- Confirm the dev server (
npm run startorwebpack --watch) is running without errors. - Remove the installed plugin from Headlamp's plugins folder and re-run
npm run start. - Restart Headlamp if hot reload does not pick up changes.
Build Errors
- Run
npm run lint(orpnpm run lint) to check for code issues. - Run
npm run tscto surface TypeScript errors. - Verify all imports resolve — shared dependencies should not be in
dependencies.
Hot Reloading Issues
- Restart the dev server.
- Avoid multiple Headlamp tabs when running in development mode.
- Clear browser cache.
- Check file permissions in the plugin directory.
Check Your Work
-
npm run build(orpnpm run build) produces output without errors - Tests pass
- Linting reports no issues
- The plugin loads in Headlamp (UI element visible, page renders if applicable)
- Browser console shows no errors related to the plugin
Next Steps
- Add a theme using
registerAppTheme— see Plugin API Reference - Deploy to a cluster — see Installing Plugins
- Review the monorepo toolchain — see Monorepo Reference
- Upstream docs: Headlamp Plugin Development
- Example plugins: Headlamp Plugin Examples