Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a20c20a4ec | |||
| 3e757db799 | |||
| 81b0b35089 | |||
| 76ab680d9a | |||
| 9aeafc4344 | |||
| e082c60677 | |||
| 96ea9e1207 | |||
| a77aa3a1dc | |||
| 145101b1b5 |
+43
-1
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.3] - 2026-02-19
|
||||
|
||||
### Changed
|
||||
|
||||
- **Package name** — renamed from `headlamp-tns-csi-plugin` to `tns-csi` so the plugin displays correctly in Headlamp's Plugins list
|
||||
|
||||
## [0.2.2] - 2026-02-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Duplicate columns** — Protocol and Pool columns on mixed-driver clusters (tns-csi + rook-ceph) are now merged into a single shared column rather than duplicated; whichever plugin loads first owns the column and the second merges into it
|
||||
- **Plugin settings name** — settings entry now registers as `tns-csi` instead of `headlamp-tns-csi-plugin`
|
||||
|
||||
## [0.2.1] - 2026-02-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- **OverviewPage crash** — brace mismatch in `TnsCsiDataContext` placed TrueNAS pool stats fetch outside the outer try block, breaking the entire context provider
|
||||
- **PV Pool column** — tns-csi driver writes `datasetName` (e.g. `pool0/pvc-abc`), not `pool`, into `volumeAttributes`; Pool is now correctly derived from the first path segment
|
||||
- **App bar badge removed** — removed the colored tns-csi status bubble from the top nav bar
|
||||
|
||||
## [0.2.0] - 2026-02-18
|
||||
|
||||
### Added
|
||||
|
||||
- **Native Headlamp integration** — Protocol/Pool/Server columns injected into the native StorageClass table; Protocol/Volume Handle columns into the native PV table
|
||||
- **PV Detail Injection** — TNS-CSI section injected into Headlamp PV detail views with full CSI volume attributes
|
||||
- **Pod Detail Injection** — Driver role/status section injected into tns-csi Pod detail pages (controller vs node role, ready status, restart count)
|
||||
- **StorageClass Benchmark button** — "Benchmark" shortcut button added to tns-csi StorageClass detail page headers
|
||||
- **App Bar Badge** — driver health badge in top nav bar showing `tns-csi: N/Nc M/Mn` (controller/node pod ready counts), color-coded green/orange/red
|
||||
- **Sidebar trim** — reduced from 6 to 4 entries (Overview, Snapshots, Metrics, Benchmark); Storage Classes and Volumes accessible via direct URL
|
||||
- **TrueNAS API integration** — WebSocket JSON-RPC client (`pool.query`) for real pool capacity (size/allocated/free/health status)
|
||||
- **Plugin settings page** — API key and server address configuration with connection test button
|
||||
- **Three-tier pool capacity display** — real TrueNAS API data → error hint → metrics-based provisioned-capacity fallback
|
||||
- **CI workflow** — lint + type-check + test on every push and PR
|
||||
- **Release workflow** — manual workflow_dispatch for versioned releases with automatic version bump, checksum, tag, and GitHub release creation
|
||||
- **Documentation** — README, CHANGELOG, CONTRIBUTING, SECURITY, and full `docs/` suite (architecture, deployment, user guide, troubleshooting)
|
||||
|
||||
## [0.1.0] - 2026-02-18
|
||||
|
||||
### Added
|
||||
@@ -30,5 +68,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- TypeScript strict mode with zero `any` types
|
||||
- ESLint + Prettier code quality tooling
|
||||
|
||||
[Unreleased]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.1.0...HEAD
|
||||
[Unreleased]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.3...HEAD
|
||||
[0.2.3]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.2...v0.2.3
|
||||
[0.2.2]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.1...v0.2.2
|
||||
[0.2.1]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/tag/v0.1.0
|
||||
|
||||
@@ -58,7 +58,7 @@ config:
|
||||
pluginsManager:
|
||||
sources:
|
||||
- name: headlamp-tns-csi-plugin
|
||||
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.1.0/headlamp-tns-csi-plugin-0.1.0.tar.gz
|
||||
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.0/headlamp-tns-csi-plugin-0.2.0.tar.gz
|
||||
```
|
||||
|
||||
Or install via the Headlamp UI:
|
||||
@@ -73,8 +73,8 @@ Or install via the Headlamp UI:
|
||||
Download the `.tar.gz` from the [GitHub releases page](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases), then extract into Headlamp's plugin directory:
|
||||
|
||||
```bash
|
||||
wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.1.0/headlamp-tns-csi-plugin-0.1.0.tar.gz
|
||||
tar xzf headlamp-tns-csi-plugin-0.1.0.tar.gz -C /headlamp/plugins/
|
||||
wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.0/headlamp-tns-csi-plugin-0.2.0.tar.gz
|
||||
tar xzf headlamp-tns-csi-plugin-0.2.0.tar.gz -C /headlamp/plugins/
|
||||
```
|
||||
|
||||
### Option 3: Build from Source
|
||||
|
||||
+5
-13
@@ -1,4 +1,4 @@
|
||||
version: "0.2.0"
|
||||
version: "0.2.3"
|
||||
name: headlamp-tns-csi-plugin
|
||||
displayName: TrueNAS CSI (tns-csi)
|
||||
description: >-
|
||||
@@ -47,19 +47,11 @@ links:
|
||||
url: https://github.com/longhorn/kbench
|
||||
|
||||
changes:
|
||||
- kind: added
|
||||
description: "Overview dashboard: driver health, storage summary, protocol distribution, non-Bound PVC alerts"
|
||||
- kind: added
|
||||
description: "Storage Classes, Volumes, Snapshots pages with slide-in detail panels"
|
||||
- kind: added
|
||||
description: "Metrics page: Prometheus WebSocket health, volume ops, CSI ops from controller pod"
|
||||
- kind: added
|
||||
description: "Benchmark page: kbench Job+PVC lifecycle, FIO log parser, results with IOPS/bandwidth/latency cards"
|
||||
- kind: added
|
||||
description: "PVC detail injection: TNS-CSI section on Headlamp PVC detail pages"
|
||||
- kind: changed
|
||||
description: "Package renamed to tns-csi so the plugin displays correctly in Headlamp's Plugins list"
|
||||
|
||||
annotations:
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.0/headlamp-tns-csi-plugin-0.2.0.tar.gz"
|
||||
headlamp/plugin/archive-checksum: "sha256:056bd353786f96627d7ff4c79e96cce40b28af3fb33daf76c4adf73d650bfffc"
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.3/tns-csi-0.2.3.tar.gz"
|
||||
headlamp/plugin/archive-checksum: "sha256:af25fda9fb90a13155ff1e37feec320dee46c0350518e227506067e73f531f81"
|
||||
headlamp/plugin/version-compat: ">=0.20.0"
|
||||
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
||||
|
||||
Binary file not shown.
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "headlamp-tns-csi-plugin",
|
||||
"version": "0.2.0",
|
||||
"name": "tns-csi",
|
||||
"version": "0.2.3",
|
||||
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -168,32 +168,32 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
setVolumeSnapshots([]);
|
||||
}
|
||||
}
|
||||
// TrueNAS pool stats (only when API key is configured)
|
||||
const config = getTnsCsiConfig();
|
||||
if (config.truenasApiKey.trim()) {
|
||||
// Determine server: explicit override → first SC server param → fail gracefully
|
||||
const server = config.truenasServerOverride.trim();
|
||||
if (server) {
|
||||
try {
|
||||
const pools = await fetchTruenasPoolStats(server, config.truenasApiKey.trim());
|
||||
if (!cancelled) {
|
||||
setPoolStats(pools);
|
||||
setPoolStatsError(null);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setPoolStats([]);
|
||||
setPoolStatsError(err instanceof Error ? err.message : String(err));
|
||||
|
||||
// TrueNAS pool stats (only when API key is configured)
|
||||
const config = getTnsCsiConfig();
|
||||
if (config.truenasApiKey.trim()) {
|
||||
const server = config.truenasServerOverride.trim();
|
||||
if (server) {
|
||||
try {
|
||||
const pools = await fetchTruenasPoolStats(server, config.truenasApiKey.trim());
|
||||
if (!cancelled) {
|
||||
setPoolStats(pools);
|
||||
setPoolStatsError(null);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setPoolStats([]);
|
||||
setPoolStatsError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!cancelled) {
|
||||
setPoolStats([]);
|
||||
setPoolStatsError(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!cancelled) {
|
||||
setPoolStats([]);
|
||||
setPoolStatsError(null);
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setAsyncError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* StorageClassColumns — registerResourceTableColumnsProcessor for StorageClass and PV tables.
|
||||
*
|
||||
* Adds Protocol/Pool/Server columns to the native /storage-classes table and
|
||||
* Protocol/Volume Handle columns to the native /persistent-volumes table.
|
||||
* Protocol/Pool columns to the native /persistent-volumes table.
|
||||
* Pool on PVs is derived from the first segment of volumeAttributes.datasetName.
|
||||
*
|
||||
* Items in column processors are KubeObject class instances from Headlamp.
|
||||
* Raw Kubernetes JSON fields (parameters, spec, status) must be accessed
|
||||
@@ -131,18 +132,21 @@ export function buildPVColumns() {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Volume Handle',
|
||||
label: 'Pool',
|
||||
getValue: (pv: unknown): string | null => {
|
||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||
if (driver !== TNS_CSI_PROVISIONER) return null;
|
||||
const h = getField(pv, 'spec', 'csi', 'volumeHandle');
|
||||
return typeof h === 'string' ? h : null;
|
||||
// tns-csi stores pool as the first segment of datasetName (e.g. "tank/pvc-abc")
|
||||
const d = getField(pv, 'spec', 'csi', 'volumeAttributes', 'datasetName');
|
||||
if (typeof d !== 'string') return null;
|
||||
return d.split('/')[0] ?? null;
|
||||
},
|
||||
render: (pv: unknown) => {
|
||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||
if (driver !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
||||
const handle = getField(pv, 'spec', 'csi', 'volumeHandle') as string | undefined;
|
||||
return <span style={{ fontFamily: 'monospace', fontSize: '0.85em' }}>{handle ?? '—'}</span>;
|
||||
const dataset = getField(pv, 'spec', 'csi', 'volumeAttributes', 'datasetName') as string | undefined;
|
||||
const pool = dataset?.split('/')[0];
|
||||
return <span>{pool ?? '—'}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
+31
-15
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
registerAppBarAction,
|
||||
registerDetailsViewHeaderAction,
|
||||
registerDetailsViewSection,
|
||||
registerPluginSettings,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
import { TnsCsiDataProvider } from './api/TnsCsiDataContext';
|
||||
import AppBarDriverBadge from './components/AppBarDriverBadge';
|
||||
import TnsCsiSettings from './components/TnsCsiSettings';
|
||||
import BenchmarkPage from './components/BenchmarkPage';
|
||||
import DriverPodDetailSection from './components/DriverPodDetailSection';
|
||||
@@ -189,12 +187,40 @@ registerDetailsViewSection(({ resource }) => {
|
||||
// Table column processors — native StorageClass and PV tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Merges incoming columns into existing ones by label.
|
||||
// If a column with the same label already exists, the incoming getValue/render
|
||||
// takes priority and falls back to the existing one (for mixed-driver tables).
|
||||
function mergeColumns<T>(
|
||||
existing: T[],
|
||||
incoming: Array<{ label: string; getValue: (r: unknown) => unknown; render: (r: unknown) => React.ReactNode }>
|
||||
): T[] {
|
||||
type ObjCol = { label: string; getValue: (r: unknown) => unknown; render: (r: unknown) => React.ReactNode };
|
||||
const isObjCol = (c: unknown): c is ObjCol =>
|
||||
typeof c === 'object' && c !== null && 'label' in c;
|
||||
const result = [...existing];
|
||||
const toAppend: typeof incoming = [];
|
||||
for (const col of incoming) {
|
||||
const idx = result.findIndex(c => isObjCol(c) && (c as ObjCol).label === col.label);
|
||||
if (idx !== -1) {
|
||||
const prev = result[idx] as ObjCol;
|
||||
result[idx] = {
|
||||
label: col.label,
|
||||
getValue: (r: unknown) => col.getValue(r) ?? prev.getValue(r),
|
||||
render: (r: unknown) => col.getValue(r) !== null ? col.render(r) : prev.render(r),
|
||||
} as unknown as T;
|
||||
} else {
|
||||
toAppend.push(col);
|
||||
}
|
||||
}
|
||||
return [...result, ...(toAppend as unknown as T[])];
|
||||
}
|
||||
|
||||
registerResourceTableColumnsProcessor(({ id, columns }) => {
|
||||
if (id === 'headlamp-storageclasses') {
|
||||
return [...columns, ...buildStorageClassColumns()];
|
||||
return mergeColumns(columns, buildStorageClassColumns());
|
||||
}
|
||||
if (id === 'headlamp-persistentvolumes') {
|
||||
return [...columns, ...buildPVColumns()];
|
||||
return mergeColumns(columns, buildPVColumns());
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
@@ -208,18 +234,8 @@ registerDetailsViewHeaderAction(({ resource }) => {
|
||||
return <StorageClassBenchmarkButton resource={resource} />;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App bar action — driver health badge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerAppBarAction(() => (
|
||||
<TnsCsiDataProvider>
|
||||
<AppBarDriverBadge />
|
||||
</TnsCsiDataProvider>
|
||||
));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerPluginSettings('headlamp-tns-csi-plugin', TnsCsiSettings, true);
|
||||
registerPluginSettings('tns-csi', TnsCsiSettings, true);
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user