Compare commits

..

8 Commits

Author SHA1 Message Date
Chris Farhood 3e757db799 chore: release v0.2.2
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>
2026-02-19 13:31:01 -05:00
Chris Farhood 81b0b35089 docs: add v0.2.1 changelog entry
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>
2026-02-19 07:07:41 -05:00
github-actions[bot] 76ab680d9a chore: release v0.2.1 2026-02-19 12:05:36 +00:00
Chris Farhood 9aeafc4344 fix: derive Pool from datasetName on tns-csi PV rows
tns-csi driver does not write pool directly into volumeAttributes; it
writes datasetName (e.g. "tank/pvc-abc123"). Extract the pool as the
first path segment so the Pool column populates correctly.

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>
2026-02-19 06:59:55 -05:00
Chris Farhood e082c60677 fix: repair brace mismatch in DataContext and replace PV Pool column with Dataset
- TnsCsiDataContext: pool stats fetch was outside the outer try block due
  to a brace mismatch introduced when adding TrueNAS API integration;
  this caused the entire fetchAsync function to throw a syntax-level
  error, breaking the OverviewPage
- StorageClassColumns (PV): replace non-populating Pool column with
  Dataset column (tns-csi driver writes datasetName, not pool, into
  PV volumeAttributes)

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>
2026-02-19 06:48:01 -05:00
Chris Farhood 96ea9e1207 feat: remove app bar driver health badge
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>
2026-02-18 19:28:00 -05:00
Chris Farhood a77aa3a1dc docs: update README download URLs to v0.2.0
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>
2026-02-18 16:58:55 -05:00
Chris Farhood 145101b1b5 docs: add v0.2.0 changelog entry and fix footer links
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>
2026-02-18 16:58:24 -05:00
8 changed files with 111 additions and 62 deletions
+36 -1
View File
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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
### 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
- 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
+3 -3
View File
@@ -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
+7 -13
View File
@@ -1,4 +1,4 @@
version: "0.2.0"
version: "0.2.2"
name: headlamp-tns-csi-plugin
displayName: TrueNAS CSI (tns-csi)
description: >-
@@ -47,19 +47,13 @@ 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: fixed
description: "Duplicate Protocol/Pool columns on mixed-driver clusters now merged into a single shared column"
- kind: fixed
description: "Plugin settings entry renamed from headlamp-tns-csi-plugin to tns-csi"
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.2/headlamp-tns-csi-plugin-0.2.2.tar.gz"
headlamp/plugin/archive-checksum: "sha256:fa6506d581ea5a609598912596995f3dd2b2501ee4f66aff5f6cd93beb525549"
headlamp/plugin/version-compat: ">=0.20.0"
headlamp/plugin/distro-compat: "in-cluster,web,app"
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "headlamp-tns-csi-plugin",
"version": "0.2.0",
"version": "0.2.2",
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
"repository": {
"type": "git",
+23 -23
View File
@@ -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
View File
@@ -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);