feat: native Headlamp integration, TrueNAS API, docs, and CI for v0.2.0
Native Headlamp integrations: - registerResourceTableColumnsProcessor: add Protocol/Pool/Server columns to native StorageClass table and Protocol/Volume Handle to PV table - registerDetailsViewSection: inject TNS-CSI section into PV detail pages - registerDetailsViewSection: inject driver role/status into tns-csi Pod pages - registerDetailsViewHeaderAction: Benchmark shortcut on StorageClass detail - registerAppBarAction: driver health badge (N/Nc M/Mn, color-coded) - Trim sidebar from 6 → 4 entries (Overview, Snapshots, Metrics, Benchmark) TrueNAS API integration: - src/api/truenas.ts: ConfigStore-backed settings, WebSocket JSON-RPC client for pool.query (auth.login_with_api_key + pool.query) - src/components/TnsCsiSettings.tsx: API key + server override settings UI with connection test button - TnsCsiDataContext: fetch real pool stats (size/allocated/free/status) - OverviewPage: three-tier pool capacity display (real data → error → metrics fallback) Documentation: - README, CHANGELOG, CONTRIBUTING, SECURITY - docs/: architecture, deployment (Helm), getting-started, user-guide, troubleshooting CI: - .github/workflows/ci.yaml: lint + type-check + test on PR/push - .github/workflows/release.yaml: workflow_dispatch versioned release 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:
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* DriverPodDetailSection — injected into Headlamp's Pod detail view.
|
||||
*
|
||||
* Shown only for tns-csi driver pods (identified by
|
||||
* app.kubernetes.io/name=tns-csi-driver label). Returns null for all other pods.
|
||||
* Uses registerDetailsViewSection in index.tsx.
|
||||
*/
|
||||
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { formatAge, isPodReady, getPodRestarts, TnsCsiPod } from '../api/k8s';
|
||||
|
||||
interface DriverPodDetailSectionProps {
|
||||
resource: {
|
||||
kind?: string;
|
||||
metadata?: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
labels?: Record<string, string>;
|
||||
creationTimestamp?: string;
|
||||
};
|
||||
spec?: { nodeName?: string };
|
||||
status?: {
|
||||
phase?: string;
|
||||
conditions?: Array<{ type: string; status: string }>;
|
||||
containerStatuses?: Array<{
|
||||
name: string;
|
||||
ready: boolean;
|
||||
restartCount: number;
|
||||
image?: string;
|
||||
state?: {
|
||||
running?: { startedAt?: string };
|
||||
waiting?: { reason?: string };
|
||||
terminated?: { exitCode?: number; reason?: string };
|
||||
};
|
||||
}>;
|
||||
};
|
||||
// KubeObject instance: raw JSON lives under jsonData;
|
||||
// metadata here only exposes what the class getter provides (labels, creationTimestamp).
|
||||
// The jsonData.metadata has the full shape.
|
||||
jsonData?: {
|
||||
metadata?: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
labels?: Record<string, string>;
|
||||
creationTimestamp?: string;
|
||||
};
|
||||
spec?: { nodeName?: string };
|
||||
status?: {
|
||||
phase?: string;
|
||||
conditions?: Array<{ type: string; status: string }>;
|
||||
containerStatuses?: Array<{
|
||||
name: string;
|
||||
ready: boolean;
|
||||
restartCount: number;
|
||||
image?: string;
|
||||
state?: {
|
||||
running?: { startedAt?: string };
|
||||
waiting?: { reason?: string };
|
||||
terminated?: { exitCode?: number; reason?: string };
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function DriverPodDetailSection({ resource }: DriverPodDetailSectionProps) {
|
||||
// Extract from jsonData (KubeObject instance) or fall back to direct props.
|
||||
// jsonData.metadata has the full shape including name/namespace; resource.metadata
|
||||
// only exposes fields that the Headlamp class getter provides (labels, creationTimestamp).
|
||||
const meta = (resource?.jsonData?.metadata ?? resource?.metadata) as {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
labels?: Record<string, string>;
|
||||
creationTimestamp?: string;
|
||||
} | undefined;
|
||||
const spec = resource?.jsonData?.spec ?? resource?.spec;
|
||||
const status = resource?.jsonData?.status ?? resource?.status;
|
||||
const labels = meta?.labels ?? {};
|
||||
|
||||
// Guard: only tns-csi driver pods
|
||||
if (labels['app.kubernetes.io/name'] !== 'tns-csi-driver') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const component = labels['app.kubernetes.io/component'] ?? 'unknown';
|
||||
const roleLabel = component === 'controller' ? 'Controller' : component === 'node' ? 'Node' : component;
|
||||
|
||||
// Build a minimal pod shape that isPodReady / getPodRestarts can consume
|
||||
const podShape: TnsCsiPod = {
|
||||
metadata: {
|
||||
name: meta?.name ?? '',
|
||||
namespace: meta?.namespace,
|
||||
creationTimestamp: meta?.creationTimestamp,
|
||||
labels,
|
||||
},
|
||||
spec: { nodeName: spec?.nodeName },
|
||||
status: status as TnsCsiPod['status'],
|
||||
};
|
||||
|
||||
const ready = isPodReady(podShape);
|
||||
const restarts = getPodRestarts(podShape);
|
||||
const phase = status?.phase ?? '—';
|
||||
const nodeName = spec?.nodeName ?? '—';
|
||||
const age = formatAge(meta?.creationTimestamp);
|
||||
|
||||
// Container statuses
|
||||
const containerStatuses = status?.containerStatuses ?? [];
|
||||
const containerRows = containerStatuses.map(cs => {
|
||||
let stateText = 'Unknown';
|
||||
if (cs.state?.running) {
|
||||
stateText = `Running since ${cs.state.running.startedAt ? formatAge(cs.state.running.startedAt) : '?'} ago`;
|
||||
} else if (cs.state?.waiting) {
|
||||
stateText = `Waiting: ${cs.state.waiting.reason ?? 'unknown'}`;
|
||||
} else if (cs.state?.terminated) {
|
||||
stateText = `Terminated (exit ${cs.state.terminated.exitCode ?? '?'}): ${cs.state.terminated.reason ?? ''}`;
|
||||
}
|
||||
return {
|
||||
name: cs.name,
|
||||
value: `${cs.ready ? '✓ Ready' : '✗ Not Ready'} — ${stateText} — ${cs.restartCount} restart(s)`,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionBox title="TNS-CSI Driver Info">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{ name: 'Role', value: roleLabel },
|
||||
{ name: 'Phase', value: phase },
|
||||
{ name: 'Ready', value: ready ? 'Yes' : 'No' },
|
||||
{ name: 'Restarts', value: String(restarts) },
|
||||
{ name: 'Node', value: nodeName },
|
||||
{ name: 'Age', value: age },
|
||||
...containerRows,
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user