feat: initial release of headlamp-rook-ceph-plugin v0.1.0

Headlamp plugin for Rook-Ceph cluster visibility.

Pages:
- Overview dashboard: CephCluster health, capacity bar, resource counts
  (block pools, filesystems, object stores, PVs, PVCs), daemon pod
  health summary, non-Bound PVC alerts
- Block Pools: CephBlockPool table with replication, failure domain,
  mirroring; slide-in detail panel
- Pods: all Rook-Ceph daemon pods grouped by role with ready/total counts

Native Headlamp integrations:
- StorageClass table: Rook Type, Pool, Cluster ID columns
- PV table: Rook Type, Pool columns
- PVC detail injection: driver, type, pool, volume handle
- PV detail injection: CSI volume attributes
- Pod detail injection: Ceph daemon role badge
- App bar badge: cluster health (HEALTH_OK/WARN/ERR), color-coded

API / architecture:
- src/api/k8s.ts: types + filters for ceph.rook.io/v1 CRDs; handles
  both default rook-ceph.* and custom-namespace provisioner strings
- src/api/RookCephDataContext.tsx: shared context provider; fetches
  CephCluster, CephBlockPool, CephFilesystem, CephObjectStore CRDs
  plus daemon pods via label selectors
- 37 unit tests (vitest + @testing-library/react)
- TypeScript strict mode, zero any types
- CI + release GitHub Actions workflows

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
2026-02-18 16:55:39 -05:00
commit 25175b65b8
30 changed files with 21588 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
/**
* CephPodDetailSection — injected into Headlamp's Pod detail view.
*
* Shown only for Rook-Ceph daemon pods (operator, mon, osd, mgr, csi).
* Guards on rook-ceph label presence.
*/
import {
NameValueTable,
SectionBox,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { formatAge, getPodRestarts } from '../api/k8s';
interface CephPodDetailSectionProps {
resource: {
metadata?: {
name?: string;
namespace?: string;
labels?: Record<string, string>;
creationTimestamp?: string;
};
spec?: { nodeName?: string; containers?: Array<{ name: string; image?: string }> };
status?: {
phase?: string;
conditions?: Array<{ type: string; status: string }>;
containerStatuses?: Array<{
name: string;
ready: boolean;
restartCount: number;
state?: {
running?: { startedAt?: string };
waiting?: { reason?: string };
terminated?: { reason?: string; exitCode?: number };
};
}>;
};
jsonData?: unknown;
};
}
const ROOK_APP_LABELS = new Set([
'rook-ceph-operator',
'rook-ceph-mon',
'rook-ceph-osd',
'rook-ceph-mgr',
'rook-ceph-mds',
'rook-ceph-rgw',
'csi-rbdplugin-provisioner',
'csi-cephfsplugin-provisioner',
'csi-rbdplugin',
'csi-cephfsplugin',
]);
const ROLE_LABELS: Record<string, string> = {
'rook-ceph-operator': 'Operator',
'rook-ceph-mon': 'Monitor (MON)',
'rook-ceph-osd': 'OSD',
'rook-ceph-mgr': 'Manager (MGR)',
'rook-ceph-mds': 'MDS (CephFS)',
'rook-ceph-rgw': 'RGW (Object Gateway)',
'csi-rbdplugin-provisioner': 'CSI RBD Provisioner',
'csi-cephfsplugin-provisioner': 'CSI CephFS Provisioner',
'csi-rbdplugin': 'CSI RBD Node Plugin',
'csi-cephfsplugin': 'CSI CephFS Node Plugin',
};
export default function CephPodDetailSection({ resource }: CephPodDetailSectionProps) {
const raw =
resource.jsonData && typeof resource.jsonData === 'object'
? (resource.jsonData as typeof resource)
: resource;
const labels = raw.metadata?.labels ?? {};
const appLabel = labels['app'] ?? '';
if (!ROOK_APP_LABELS.has(appLabel)) return null;
const role = ROLE_LABELS[appLabel] ?? appLabel;
const phase = raw.status?.phase ?? 'Unknown';
const isReady =
raw.status?.conditions?.some((c) => c.type === 'Ready' && c.status === 'True') ?? false;
const restarts =
raw.status?.containerStatuses?.reduce((s, c) => s + c.restartCount, 0) ?? 0;
const containerRows = (raw.status?.containerStatuses ?? []).map((cs) => {
let stateStr = 'Unknown';
if (cs.state?.running) stateStr = 'Running';
else if (cs.state?.waiting) stateStr = `Waiting: ${cs.state.waiting.reason ?? ''}`;
else if (cs.state?.terminated)
stateStr = `Terminated: ${cs.state.terminated.reason ?? ''} (exit ${cs.state.terminated.exitCode ?? ''})`;
return {
name: cs.name,
value: (
<StatusLabel status={cs.ready ? 'success' : 'warning'}>
{stateStr} | Restarts: {cs.restartCount}
</StatusLabel>
),
};
});
return (
<SectionBox title="Rook-Ceph Daemon Info">
<NameValueTable
rows={[
{
name: 'Role',
value: <StatusLabel status="success">{role}</StatusLabel>,
},
{
name: 'Phase',
value: (
<StatusLabel status={isReady ? 'success' : 'error'}>
{phase}
</StatusLabel>
),
},
{ name: 'Node', value: raw.spec?.nodeName ?? '—' },
{ name: 'Restarts', value: String(restarts) },
{ name: 'Age', value: formatAge(raw.metadata?.creationTimestamp) },
...containerRows,
]}
/>
</SectionBox>
);
}