Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e757db799 | |||
| 81b0b35089 | |||
| 76ab680d9a | |||
| 9aeafc4344 | |||
| e082c60677 | |||
| 96ea9e1207 | |||
| a77aa3a1dc | |||
| 145101b1b5 |
+36
-1
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
## [0.1.0] - 2026-02-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -30,5 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- TypeScript strict mode with zero `any` types
|
- TypeScript strict mode with zero `any` types
|
||||||
- ESLint + Prettier code quality tooling
|
- 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.2...HEAD
|
||||||
|
[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
|
[0.1.0]: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/tag/v0.1.0
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ config:
|
|||||||
pluginsManager:
|
pluginsManager:
|
||||||
sources:
|
sources:
|
||||||
- name: headlamp-tns-csi-plugin
|
- 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:
|
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:
|
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
|
```bash
|
||||||
wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.1.0/headlamp-tns-csi-plugin-0.1.0.tar.gz
|
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.1.0.tar.gz -C /headlamp/plugins/
|
tar xzf headlamp-tns-csi-plugin-0.2.0.tar.gz -C /headlamp/plugins/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 3: Build from Source
|
### Option 3: Build from Source
|
||||||
|
|||||||
+7
-13
@@ -1,4 +1,4 @@
|
|||||||
version: "0.2.0"
|
version: "0.2.2"
|
||||||
name: headlamp-tns-csi-plugin
|
name: headlamp-tns-csi-plugin
|
||||||
displayName: TrueNAS CSI (tns-csi)
|
displayName: TrueNAS CSI (tns-csi)
|
||||||
description: >-
|
description: >-
|
||||||
@@ -47,19 +47,13 @@ links:
|
|||||||
url: https://github.com/longhorn/kbench
|
url: https://github.com/longhorn/kbench
|
||||||
|
|
||||||
changes:
|
changes:
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Overview dashboard: driver health, storage summary, protocol distribution, non-Bound PVC alerts"
|
description: "Duplicate Protocol/Pool columns on mixed-driver clusters now merged into a single shared column"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Storage Classes, Volumes, Snapshots pages with slide-in detail panels"
|
description: "Plugin settings entry renamed from headlamp-tns-csi-plugin to tns-csi"
|
||||||
- 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"
|
|
||||||
|
|
||||||
annotations:
|
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-url: "https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.2.2/headlamp-tns-csi-plugin-0.2.2.tar.gz"
|
||||||
headlamp/plugin/archive-checksum: "sha256:056bd353786f96627d7ff4c79e96cce40b28af3fb33daf76c4adf73d650bfffc"
|
headlamp/plugin/archive-checksum: "sha256:fa6506d581ea5a609598912596995f3dd2b2501ee4f66aff5f6cd93beb525549"
|
||||||
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"
|
||||||
|
|||||||
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-tns-csi-plugin",
|
"name": "headlamp-tns-csi-plugin",
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
|
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -168,32 +168,32 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
|||||||
setVolumeSnapshots([]);
|
setVolumeSnapshots([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TrueNAS pool stats (only when API key is configured)
|
|
||||||
const config = getTnsCsiConfig();
|
// TrueNAS pool stats (only when API key is configured)
|
||||||
if (config.truenasApiKey.trim()) {
|
const config = getTnsCsiConfig();
|
||||||
// Determine server: explicit override → first SC server param → fail gracefully
|
if (config.truenasApiKey.trim()) {
|
||||||
const server = config.truenasServerOverride.trim();
|
const server = config.truenasServerOverride.trim();
|
||||||
if (server) {
|
if (server) {
|
||||||
try {
|
try {
|
||||||
const pools = await fetchTruenasPoolStats(server, config.truenasApiKey.trim());
|
const pools = await fetchTruenasPoolStats(server, config.truenasApiKey.trim());
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPoolStats(pools);
|
setPoolStats(pools);
|
||||||
setPoolStatsError(null);
|
setPoolStatsError(null);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPoolStats([]);
|
setPoolStats([]);
|
||||||
setPoolStatsError(err instanceof Error ? err.message : String(err));
|
setPoolStatsError(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (!cancelled) {
|
||||||
|
setPoolStats([]);
|
||||||
|
setPoolStatsError(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} catch (err: unknown) {
|
||||||
if (!cancelled) {
|
|
||||||
setPoolStats([]);
|
|
||||||
setPoolStatsError(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setAsyncError(err instanceof Error ? err.message : String(err));
|
setAsyncError(err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
* StorageClassColumns — registerResourceTableColumnsProcessor for StorageClass and PV tables.
|
* StorageClassColumns — registerResourceTableColumnsProcessor for StorageClass and PV tables.
|
||||||
*
|
*
|
||||||
* Adds Protocol/Pool/Server columns to the native /storage-classes table and
|
* 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.
|
* Items in column processors are KubeObject class instances from Headlamp.
|
||||||
* Raw Kubernetes JSON fields (parameters, spec, status) must be accessed
|
* 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 => {
|
getValue: (pv: unknown): string | null => {
|
||||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||||
if (driver !== TNS_CSI_PROVISIONER) return null;
|
if (driver !== TNS_CSI_PROVISIONER) return null;
|
||||||
const h = getField(pv, 'spec', 'csi', 'volumeHandle');
|
// tns-csi stores pool as the first segment of datasetName (e.g. "tank/pvc-abc")
|
||||||
return typeof h === 'string' ? h : null;
|
const d = getField(pv, 'spec', 'csi', 'volumeAttributes', 'datasetName');
|
||||||
|
if (typeof d !== 'string') return null;
|
||||||
|
return d.split('/')[0] ?? null;
|
||||||
},
|
},
|
||||||
render: (pv: unknown) => {
|
render: (pv: unknown) => {
|
||||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||||
if (driver !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
if (driver !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
||||||
const handle = getField(pv, 'spec', 'csi', 'volumeHandle') as string | undefined;
|
const dataset = getField(pv, 'spec', 'csi', 'volumeAttributes', 'datasetName') as string | undefined;
|
||||||
return <span style={{ fontFamily: 'monospace', fontSize: '0.85em' }}>{handle ?? '—'}</span>;
|
const pool = dataset?.split('/')[0];
|
||||||
|
return <span>{pool ?? '—'}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+31
-15
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
registerAppBarAction,
|
|
||||||
registerDetailsViewHeaderAction,
|
registerDetailsViewHeaderAction,
|
||||||
registerDetailsViewSection,
|
registerDetailsViewSection,
|
||||||
registerPluginSettings,
|
registerPluginSettings,
|
||||||
@@ -16,7 +15,6 @@ import {
|
|||||||
} from '@kinvolk/headlamp-plugin/lib';
|
} from '@kinvolk/headlamp-plugin/lib';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TnsCsiDataProvider } from './api/TnsCsiDataContext';
|
import { TnsCsiDataProvider } from './api/TnsCsiDataContext';
|
||||||
import AppBarDriverBadge from './components/AppBarDriverBadge';
|
|
||||||
import TnsCsiSettings from './components/TnsCsiSettings';
|
import TnsCsiSettings from './components/TnsCsiSettings';
|
||||||
import BenchmarkPage from './components/BenchmarkPage';
|
import BenchmarkPage from './components/BenchmarkPage';
|
||||||
import DriverPodDetailSection from './components/DriverPodDetailSection';
|
import DriverPodDetailSection from './components/DriverPodDetailSection';
|
||||||
@@ -189,12 +187,40 @@ registerDetailsViewSection(({ resource }) => {
|
|||||||
// Table column processors — native StorageClass and PV tables
|
// 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 }) => {
|
registerResourceTableColumnsProcessor(({ id, columns }) => {
|
||||||
if (id === 'headlamp-storageclasses') {
|
if (id === 'headlamp-storageclasses') {
|
||||||
return [...columns, ...buildStorageClassColumns()];
|
return mergeColumns(columns, buildStorageClassColumns());
|
||||||
}
|
}
|
||||||
if (id === 'headlamp-persistentvolumes') {
|
if (id === 'headlamp-persistentvolumes') {
|
||||||
return [...columns, ...buildPVColumns()];
|
return mergeColumns(columns, buildPVColumns());
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
});
|
});
|
||||||
@@ -208,18 +234,8 @@ registerDetailsViewHeaderAction(({ resource }) => {
|
|||||||
return <StorageClassBenchmarkButton resource={resource} />;
|
return <StorageClassBenchmarkButton resource={resource} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// App bar action — driver health badge
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
registerAppBarAction(() => (
|
|
||||||
<TnsCsiDataProvider>
|
|
||||||
<AppBarDriverBadge />
|
|
||||||
</TnsCsiDataProvider>
|
|
||||||
));
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Plugin settings
|
// Plugin settings
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
registerPluginSettings('headlamp-tns-csi-plugin', TnsCsiSettings, true);
|
registerPluginSettings('tns-csi', TnsCsiSettings, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user