Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a20c20a4ec | |||
| 3e757db799 | |||
| 81b0b35089 | |||
| 76ab680d9a | |||
| 9aeafc4344 | |||
| e082c60677 | |||
| 96ea9e1207 | |||
| a77aa3a1dc | |||
| 145101b1b5 | |||
| 03d616a545 | |||
| 7ef7b8b7b5 | |||
| f1feb5c2f7 | |||
| f2f3c3a87e | |||
| aeab42b6ec | |||
| 44a7f654f0 | |||
| 0d0bf0f609 |
@@ -0,0 +1,37 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Lint
|
||||
run: npx eslint --ext .ts,.tsx src/
|
||||
|
||||
- name: Type-check
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (without v prefix, e.g., 0.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "::error::Version must be in format X.Y.Z (e.g., 0.2.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Update package.json version
|
||||
run: |
|
||||
jq --arg version "${{ inputs.version }}" '.version = $version' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
- name: Update artifacthub-pkg.yml version and URL
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/headlamp-tns-csi-plugin-${VERSION}.tar.gz"
|
||||
|
||||
sed -i "s|^version:.*|version: \"${VERSION}\"|" artifacthub-pkg.yml
|
||||
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Package plugin
|
||||
run: npx @kinvolk/headlamp-plugin package
|
||||
|
||||
- name: Validate tarball name
|
||||
run: |
|
||||
EXPECTED="headlamp-tns-csi-plugin-${{ inputs.version }}.tar.gz"
|
||||
ACTUAL=$(ls *.tar.gz)
|
||||
if [ "$EXPECTED" != "$ACTUAL" ]; then
|
||||
echo "::error::Tarball name mismatch! Expected: $EXPECTED, Got: $ACTUAL"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Tarball name validated: $ACTUAL"
|
||||
|
||||
- name: Compute checksum
|
||||
id: compute_checksum
|
||||
run: |
|
||||
TARBALL="headlamp-tns-csi-plugin-${{ inputs.version }}.tar.gz"
|
||||
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
|
||||
echo "Checksum: sha256:${CHECKSUM}"
|
||||
|
||||
- name: Update checksum in metadata
|
||||
run: |
|
||||
CHECKSUM="${{ steps.compute_checksum.outputs.checksum }}"
|
||||
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: \"sha256:${CHECKSUM}\"|" artifacthub-pkg.yml
|
||||
|
||||
- name: Commit version bump and metadata
|
||||
run: |
|
||||
git add package.json artifacthub-pkg.yml
|
||||
git commit -m "chore: release v${{ inputs.version }}"
|
||||
git push origin main
|
||||
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
git tag "v${{ inputs.version }}"
|
||||
git push origin "v${{ inputs.version }}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: "v${{ inputs.version }}"
|
||||
files: headlamp-tns-csi-plugin-${{ inputs.version }}.tar.gz
|
||||
fail_on_unmatched_files: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✓ Version bumped to ${{ inputs.version }}"
|
||||
echo "✓ Metadata updated with checksum sha256:${{ steps.compute_checksum.outputs.checksum }}"
|
||||
echo "✓ Tag v${{ inputs.version }} created"
|
||||
echo "✓ GitHub release published with tarball"
|
||||
@@ -0,0 +1,76 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Headlamp TNS-CSI Plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [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
|
||||
|
||||
- **Overview Dashboard** — driver health card, storage summary (StorageClass / PV / PVC counts), protocol distribution with PercentageBar, non-Bound PVC alert table, and live Prometheus metric snapshot
|
||||
- **Storage Classes page** — table with Protocol, Pool, Server, Reclaim Policy, Expansion, and PV count columns; slide-in detail panel with protocol-specific prerequisite notes (NFS, NVMe-oF, iSCSI)
|
||||
- **Volumes page** — PersistentVolume table with capacity, access modes, reclaim policy, phase status badge, and bound claim; slide-in detail panel with full CSI volume attributes
|
||||
- **Snapshots page** — VolumeSnapshot table scoped to tns-csi VolumeSnapshotClasses; graceful degradation when snapshot CRD is not installed
|
||||
- **Metrics page** — Prometheus WebSocket health indicator, per-volume I/O (read/write IOPS and bandwidth), CSI operation latency cards from controller pod port 8080
|
||||
- **Benchmark page** — interactive kbench runner with StorageClass selection, capacity/access-mode configuration, Job+PVC lifecycle management, live FIO log streaming, and IOPS/bandwidth/latency result cards
|
||||
- **PVC Detail Injection** — TNS-CSI section automatically injected into Headlamp's PVC detail views showing protocol, server, pool, volume handle, and link to the bound PV
|
||||
- **TnsCsiDataContext** — shared React context provider for all plugin pages; extracts `jsonData` from Headlamp KubeObject instances so StorageClass `parameters` (protocol, pool, server) are accessible
|
||||
- **Prometheus text format parser** — zero-dependency parser for the Prometheus exposition format used by the tns-csi controller
|
||||
- **kbench FIO log parser** — parses `yasker/kbench` FIO output into structured IOPS, bandwidth (MB/s), and latency (µs) results
|
||||
- **ArtifactHub publishing** — `artifacthub-pkg.yml` and `artifacthub-repo.yml` registered at Artifact Hub; plugin available in Headlamp catalog
|
||||
|
||||
### Infrastructure
|
||||
|
||||
- GitHub repository setup with initial CI and release workflows
|
||||
- 67 unit tests with Vitest + @testing-library/react
|
||||
- TypeScript strict mode with zero `any` types
|
||||
- ESLint + Prettier code quality tooling
|
||||
|
||||
[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
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
# Contributing to Headlamp TNS-CSI Plugin
|
||||
|
||||
Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Branching Strategy](#branching-strategy)
|
||||
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Code Style](#code-style)
|
||||
- [Testing Requirements](#testing-requirements)
|
||||
- [Documentation](#documentation)
|
||||
- [Release Process](#release-process)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows a standard code of conduct:
|
||||
- Be respectful and inclusive
|
||||
- Welcome newcomers and help them get started
|
||||
- Focus on constructive feedback
|
||||
- Assume good intentions
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20 or later
|
||||
- npm
|
||||
- Access to a Kubernetes cluster with Headlamp and tns-csi installed (for end-to-end testing)
|
||||
- Git
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Fork and clone the repository:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/headlamp-tns-csi-plugin.git
|
||||
cd headlamp-tns-csi-plugin
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Start development mode:**
|
||||
```bash
|
||||
npm start
|
||||
# Plugin will be available at http://localhost:4466
|
||||
```
|
||||
|
||||
4. **Run tests:**
|
||||
```bash
|
||||
npm test # 67 unit tests
|
||||
npm run tsc # TypeScript type-check
|
||||
```
|
||||
|
||||
5. **Build the plugin:**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Feature Development
|
||||
|
||||
1. Create a feature branch from `main`
|
||||
2. Make your changes
|
||||
3. Write/update tests
|
||||
4. Update documentation
|
||||
5. Run lint and tests locally
|
||||
6. Submit a pull request
|
||||
|
||||
### Local Testing
|
||||
|
||||
**Option 1: Development Mode**
|
||||
```bash
|
||||
npm start
|
||||
# Opens Headlamp at http://localhost:4466 with hot reload
|
||||
```
|
||||
|
||||
**Option 2: Production Build**
|
||||
```bash
|
||||
npm run build
|
||||
npm run package
|
||||
# Installs the packaged tarball into a running Headlamp instance
|
||||
```
|
||||
|
||||
### Connecting to a Real Cluster
|
||||
|
||||
The plugin requires a running tns-csi driver to display meaningful data. For development:
|
||||
|
||||
1. Configure `KUBECONFIG` to point at a cluster with tns-csi installed
|
||||
2. Run `npm start` — Headlamp dev server will proxy API requests through your kubeconfig
|
||||
3. The Benchmark page requires RBAC permissions for Jobs and PVCs in the target namespace
|
||||
|
||||
## Branching Strategy
|
||||
|
||||
### Main Branch
|
||||
|
||||
- **Purpose:** Stable, production-ready code
|
||||
- **Protection:** Only merge via pull requests
|
||||
- **Naming:** `main`
|
||||
|
||||
### Feature Branches
|
||||
|
||||
- **Purpose:** Development of new features or fixes
|
||||
- **Naming Convention:**
|
||||
- Features: `feat/description`
|
||||
- Bug fixes: `fix/description`
|
||||
- Documentation: `docs/description`
|
||||
- Refactoring: `refactor/description`
|
||||
- Chores: `chore/description`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
feat/add-volume-clone-support
|
||||
fix/metrics-page-empty-on-restart
|
||||
docs/update-rbac-guide
|
||||
refactor/kbench-state-machine
|
||||
chore/upgrade-dependencies
|
||||
```
|
||||
|
||||
### Branching Rules
|
||||
|
||||
**✅ ALWAYS use feature branches for:**
|
||||
- Code changes (new features, bug fixes, refactors)
|
||||
- Test updates
|
||||
- CI/CD workflow changes
|
||||
- Dependency updates
|
||||
|
||||
**✅ MAY push directly to main for:**
|
||||
- Documentation-only changes (README, CLAUDE.md, comments)
|
||||
- Version bump commits (`package.json` + `artifacthub-pkg.yml`)
|
||||
|
||||
**❌ NEVER push directly to main for:**
|
||||
- Any code changes to `src/`
|
||||
- Test file changes
|
||||
- Workflow changes
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
We follow [Conventional Commits](https://www.conventionalcommits.org/) format:
|
||||
|
||||
### Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- **feat:** New feature
|
||||
- **fix:** Bug fix
|
||||
- **docs:** Documentation only
|
||||
- **style:** Code style (formatting, no logic change)
|
||||
- **refactor:** Code change that neither fixes a bug nor adds a feature
|
||||
- **perf:** Performance improvement
|
||||
- **test:** Adding or updating tests
|
||||
- **chore:** Maintenance tasks (deps, build, CI)
|
||||
- **ci:** CI/CD changes
|
||||
|
||||
### Scope (Optional)
|
||||
|
||||
- `api` — API-related changes
|
||||
- `ui` — UI component changes
|
||||
- `metrics` — Prometheus metrics changes
|
||||
- `kbench` — Benchmark changes
|
||||
- `tests` — Test-related changes
|
||||
- `docs` — Documentation changes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
feat(ui): add PV clone button to Volumes detail panel
|
||||
|
||||
fix(api): extract jsonData from headlamp KubeObject instances for parameter access
|
||||
|
||||
docs: add RBAC examples for Benchmark page
|
||||
|
||||
chore: bump version to 0.2.0
|
||||
|
||||
test(kbench): add FIO log parser edge case tests
|
||||
```
|
||||
|
||||
### Footer
|
||||
|
||||
Add `Co-Authored-By` for pair programming or AI assistance:
|
||||
|
||||
```
|
||||
feat: add NVMe-oF protocol notes to StorageClass detail panel
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||||
Co-Authored-By: Happy <yesreply@happy.engineering>
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Creating a PR
|
||||
|
||||
1. **Run all checks locally:**
|
||||
```bash
|
||||
npm run build # Verify build succeeds
|
||||
npm run lint # Check for linting errors
|
||||
npm run tsc # Type-check TypeScript
|
||||
npm test # Run 67 unit tests
|
||||
```
|
||||
|
||||
2. **Update documentation:**
|
||||
- Update README.md if you added features or changed behavior
|
||||
- Update CLAUDE.md if you changed architecture or constraints
|
||||
- Add JSDoc comments for new exported APIs
|
||||
|
||||
3. **Write/update tests:**
|
||||
- Add unit tests for new functions/components
|
||||
- Ensure all 67 tests (plus yours) pass
|
||||
|
||||
### Creating a PR
|
||||
|
||||
1. **Push your branch:**
|
||||
```bash
|
||||
git push origin feat/your-feature
|
||||
```
|
||||
|
||||
2. **Create PR on GitHub:**
|
||||
- Use a descriptive title following commit conventions
|
||||
- Link related issues with `Fixes #123` or `Closes #456`
|
||||
|
||||
3. **PR Title Format:**
|
||||
```
|
||||
feat: add VolumeSnapshot creation from Volumes page
|
||||
fix: correct FIO log parser for multi-job output
|
||||
docs: improve Benchmark RBAC setup guide
|
||||
```
|
||||
|
||||
4. **PR Description Should Include:**
|
||||
- Summary of changes
|
||||
- Motivation and context
|
||||
- Testing performed (which cluster/driver version)
|
||||
- Screenshots for UI changes
|
||||
- Breaking changes (if any)
|
||||
|
||||
### PR Review Process
|
||||
|
||||
1. **Automated Checks:**
|
||||
- ✅ CI workflow (lint, type-check, build, test)
|
||||
|
||||
2. **Maintainer Review:**
|
||||
- Code quality and style
|
||||
- Test coverage
|
||||
- Documentation completeness
|
||||
- No new `any` types introduced
|
||||
|
||||
3. **Merging:**
|
||||
- Use **merge commits** (not squash, not rebase)
|
||||
- Delete feature branch after merge
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript
|
||||
|
||||
- **Strictness:** Full TypeScript strict mode — zero `any` types
|
||||
- **Unknown at boundaries:** Use `unknown` + type guards at API boundaries (headlamp hooks, ApiProxy responses)
|
||||
- **Interfaces over types:** Prefer `interface` for object shapes
|
||||
- **No class components:** Functional components with hooks only
|
||||
|
||||
### React
|
||||
|
||||
- **Functional components only** — no class components
|
||||
- **Props interfaces:** Always define props as named interfaces
|
||||
- **Headlamp components:** Use only `@kinvolk/headlamp-plugin/lib/CommonComponents` — no direct MUI imports
|
||||
- **Detail panels:** Follow the slide-in drawer pattern — URL hash state, Escape to close, backdrop overlay
|
||||
|
||||
### Headlamp KubeObject Access
|
||||
|
||||
Headlamp's `useList()` hooks return KubeObject class instances that store raw JSON under `.jsonData`. Always extract `jsonData` before passing objects to plain-object type helpers:
|
||||
|
||||
```typescript
|
||||
// ✅ Correct — extract jsonData so .parameters, .spec, .status are accessible
|
||||
const rawItems = items.map(item =>
|
||||
item && typeof item === 'object' && 'jsonData' in item
|
||||
? (item as { jsonData: unknown }).jsonData
|
||||
: item
|
||||
);
|
||||
|
||||
// ❌ Wrong — sc.parameters will be undefined on KubeObject instances
|
||||
const scs = (allStorageClasses as unknown[]).filter(isTnsCsiStorageClass);
|
||||
```
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
npm run lint # ESLint
|
||||
npm run tsc # TypeScript check
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Components:** PascalCase (`OverviewPage`, `BenchmarkPage`)
|
||||
- **Files:** Match component name (`OverviewPage.tsx`)
|
||||
- **Hooks:** Prefix with `use` (`useTnsCsiContext`)
|
||||
- **Utilities:** camelCase (`formatProtocol`, `parsePrometheusText`)
|
||||
- **Constants:** UPPER_SNAKE_CASE (`TNS_CSI_PROVISIONER`)
|
||||
|
||||
### Import Organization
|
||||
|
||||
1. React imports
|
||||
2. Third-party libraries
|
||||
3. Headlamp plugin imports (`@kinvolk/headlamp-plugin/lib`)
|
||||
4. Local imports (components, API, types)
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests (Required)
|
||||
|
||||
All 67 tests must pass before committing:
|
||||
|
||||
```bash
|
||||
npm test # vitest run
|
||||
npm run tsc # must exit 0
|
||||
```
|
||||
|
||||
- All new functions must have unit tests
|
||||
- Bug fixes should include regression tests
|
||||
- Use descriptive test names
|
||||
|
||||
**Mock pattern for headlamp APIs:**
|
||||
|
||||
```typescript
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: { request: vi.fn().mockResolvedValue({ items: [] }) },
|
||||
K8s: {
|
||||
ResourceClasses: {
|
||||
StorageClass: { useList: vi.fn(() => [[], null]) },
|
||||
PersistentVolume: { useList: vi.fn(() => [[], null]) },
|
||||
PersistentVolumeClaim: { useList: vi.fn(() => [[], null]) },
|
||||
},
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Test File Structure
|
||||
|
||||
```
|
||||
src/api/k8s.test.ts -- Type guards, filter helpers, format utilities
|
||||
src/api/metrics.test.ts -- Prometheus text parser
|
||||
src/api/kbench.test.ts -- FIO log parser, manifest builders, format helpers
|
||||
src/api/TnsCsiDataContext.test.tsx -- Context provider integration
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Documentation Updates Required
|
||||
|
||||
When making changes, update relevant documentation:
|
||||
|
||||
#### Code Changes
|
||||
- **README.md** — User-facing features, installation, configuration, troubleshooting table
|
||||
- **CLAUDE.md** — Architecture constraints, key constants, subagent guidance
|
||||
- **CHANGELOG.md** — Add entry under `[Unreleased]`
|
||||
- **JSDoc** — All exported functions and components
|
||||
|
||||
#### Architecture Changes
|
||||
- **docs/architecture/overview.md** — If the data flow or component structure changes
|
||||
- **CLAUDE.md** — Update architecture section
|
||||
|
||||
### JSDoc Style
|
||||
|
||||
Use JSDoc for all exported functions and types:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Parses Prometheus text format exposition into a flat key→value map.
|
||||
*
|
||||
* Ignores comment lines and HELP/TYPE metadata. Returns only the last
|
||||
* sample value for each unique metric+label combination.
|
||||
*
|
||||
* @param text - Raw Prometheus text format string
|
||||
* @returns Map of metric name (with labels) to numeric value
|
||||
*/
|
||||
export function parsePrometheusText(text: string): Map<string, number> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
### Version Numbering
|
||||
|
||||
We follow [Semantic Versioning](https://semver.org/):
|
||||
- **Major (1.0.0):** Breaking changes
|
||||
- **Minor (0.1.0):** New features, backward compatible
|
||||
- **Patch (0.0.1):** Bug fixes, backward compatible
|
||||
|
||||
### Creating a Release (Maintainers Only)
|
||||
|
||||
1. **Update CHANGELOG.md:**
|
||||
- Move items from `[Unreleased]` to a new `[X.Y.Z] - YYYY-MM-DD` section
|
||||
|
||||
2. **Trigger the release workflow:**
|
||||
- Go to **Actions → Release → Run workflow**
|
||||
- Enter the version number (e.g., `0.2.0`)
|
||||
|
||||
3. **GitHub Actions automatically:**
|
||||
- Updates `package.json` and `artifacthub-pkg.yml` version
|
||||
- Builds plugin tarball
|
||||
- Computes SHA256 checksum and updates metadata
|
||||
- Commits, creates tag, and publishes GitHub release
|
||||
|
||||
4. **ArtifactHub syncs within 30 minutes**
|
||||
|
||||
### Pre-release Versions
|
||||
|
||||
For testing before stable release, use `-rc.N` suffix: `v0.2.0-rc.1`. Mark as "pre-release" on GitHub.
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Questions:** Open a [GitHub Discussion](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/discussions)
|
||||
- **Bugs:** Open a [GitHub Issue](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/issues)
|
||||
- **Architecture:** See [CLAUDE.md](CLAUDE.md) and [docs/architecture/overview.md](docs/architecture/overview.md)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the Apache-2.0 License.
|
||||
@@ -0,0 +1,296 @@
|
||||
# Headlamp TNS-CSI Plugin
|
||||
|
||||
[](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin)
|
||||
[](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/actions/workflows/ci.yaml)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
A [Headlamp](https://headlamp.dev/) plugin that surfaces [tns-csi](https://github.com/fenio/tns-csi) CSI driver visibility and kbench storage benchmarking directly in the Headlamp UI.
|
||||
|
||||
**[Documentation](#documentation) | [Installation](#installing) | [Security](#rbac--security-setup) | [Development](#development)**
|
||||
|
||||
## What It Does
|
||||
|
||||
Adds a **TrueNAS (tns-csi)** top-level sidebar section to Headlamp with full CSI driver observability and interactive storage benchmarking:
|
||||
|
||||
### Main Views
|
||||
|
||||
- **Overview Dashboard** — driver health card, storage summary (StorageClass / PV / PVC counts), protocol distribution, PercentageBar for Bound vs non-Bound PVCs, non-Bound PVC alert table, and live Prometheus metric snapshot
|
||||
- **Storage Classes** — table of tns-csi StorageClasses with Protocol, Pool, Server, Reclaim Policy, Expansion, and PV count columns; click a row for a slide-in detail panel including protocol-specific prerequisite notes
|
||||
- **Volumes** — table of tns-csi PersistentVolumes with capacity, access modes, reclaim policy, status badge, and bound claim; slide-in detail panel with full CSI volume attributes
|
||||
- **Snapshots** — table of VolumeSnapshots scoped to tns-csi VolumeSnapshotClasses; shows ready status, size, source PVC, and class; graceful degradation when snapshot CRD is absent
|
||||
- **Metrics** — Prometheus WebSocket health indicator, per-volume I/O (read/write IOPS and bandwidth from the controller pod), and CSI operation latency cards
|
||||
- **Benchmark** — interactive kbench runner: select a tns-csi StorageClass, configure capacity and access mode, then run/stop a kbench Job+PVC lifecycle; live FIO log streaming with IOPS, bandwidth, and latency result cards
|
||||
|
||||
### Integrated Features
|
||||
|
||||
- **PVC Detail Injection** — TNS-CSI section automatically injected into Headlamp's PVC detail views showing protocol, server, pool, volume handle, and link to the bound PV
|
||||
- **Dark Mode Support** — full theme adaptation using MUI CSS variables across all panels and drawers
|
||||
- **Graceful Degradation** — Snapshot CRD absence is detected silently; missing Prometheus data shows placeholder cards rather than errors
|
||||
- **kbench Lifecycle Management** — automatically creates and cleans up the benchmark Job and PVC; `app.kubernetes.io/managed-by=headlamp-tns-csi-plugin` label guards all managed resources
|
||||
|
||||
### Data & Refresh
|
||||
|
||||
StorageClasses, PersistentVolumes, and PVCs are fetched via Headlamp's `K8s.ResourceClasses` hooks (live watch). Driver pods, the CSIDriver object, VolumeSnapshots, and Prometheus metrics are fetched via `ApiProxy.request`. Metrics are polled from the tns-csi controller pod at port `8080` using the Prometheus text format parser.
|
||||
|
||||
The plugin is **read-only** except for the Benchmark page, which creates and deletes a Job and PVC in the namespace you select.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Minimum version |
|
||||
| --------------------- | --------------- |
|
||||
| Headlamp | v0.20+ |
|
||||
| tns-csi driver | Any release |
|
||||
| Kubernetes | v1.24+ |
|
||||
| snapshot CRD (optional) | v1 |
|
||||
|
||||
The tns-csi driver must be deployed in `kube-system` with the standard `app.kubernetes.io/name=tns-csi-driver` labels. The controller pod must expose Prometheus metrics on port `8080`.
|
||||
|
||||
## Installing
|
||||
|
||||
### Option 1: Headlamp Plugin Manager (Recommended)
|
||||
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin). Configure Headlamp via Helm:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
pluginsManager:
|
||||
sources:
|
||||
- name: headlamp-tns-csi-plugin
|
||||
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:
|
||||
|
||||
1. Go to **Settings → Plugins**
|
||||
2. Click **Catalog** tab
|
||||
3. Search for "TNS CSI" or "TrueNAS"
|
||||
4. Click **Install**
|
||||
|
||||
### Option 2: Manual Tarball Install
|
||||
|
||||
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.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
|
||||
|
||||
```bash
|
||||
git clone https://github.com/privilegedescalation/headlamp-tns-csi-plugin.git
|
||||
cd headlamp-tns-csi-plugin
|
||||
npm install
|
||||
npm run build
|
||||
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||
```
|
||||
|
||||
## RBAC / Security Setup
|
||||
|
||||
The plugin reads from the Kubernetes API and the tns-csi controller pod's Prometheus endpoint. The Benchmark page additionally creates and deletes Jobs and PVCs.
|
||||
|
||||
### Minimal read-only permissions
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: headlamp-tns-csi-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumes", "persistentvolumeclaims", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses", "csidrivers"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["snapshot.storage.k8s.io"]
|
||||
resources: ["volumesnapshots", "volumesnapshotclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log"]
|
||||
verbs: ["get"]
|
||||
```
|
||||
|
||||
### Additional permissions for Benchmark page
|
||||
|
||||
```yaml
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["create", "delete"]
|
||||
```
|
||||
|
||||
### Metrics access
|
||||
|
||||
The plugin fetches Prometheus metrics from the tns-csi controller pod via the Kubernetes pod proxy sub-resource. Grant `get` on `pods/proxy` in `kube-system`:
|
||||
|
||||
```yaml
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/proxy"]
|
||||
verbs: ["get"]
|
||||
# Optionally scope to the controller pod namespace
|
||||
```
|
||||
|
||||
Apply the role and bind it to your Headlamp service account with a ClusterRoleBinding.
|
||||
|
||||
## Documentation
|
||||
|
||||
**[Complete Documentation](docs/README.md)** — Documentation hub with all guides
|
||||
|
||||
### Quick Links
|
||||
|
||||
- **[Architecture](docs/architecture/overview.md)** — System architecture, data flow, component hierarchy
|
||||
- **[Deployment](docs/deployment/helm.md)** — Production deployment with Helm, FluxCD
|
||||
- **[Troubleshooting](docs/troubleshooting/README.md)** — Common issues and diagnosis
|
||||
- **[Contributing](CONTRIBUTING.md)** — Development workflow, branching strategy, PR process
|
||||
- **[Security](SECURITY.md)** — Security model, RBAC, vulnerability reporting
|
||||
- **[Changelog](CHANGELOG.md)** — Complete release history
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**For comprehensive troubleshooting, see [docs/troubleshooting/README.md](docs/troubleshooting/README.md).**
|
||||
|
||||
Quick reference:
|
||||
|
||||
| Symptom | Likely Cause | Quick Fix |
|
||||
| ------- | ------------ | --------- |
|
||||
| **Plugin not in sidebar** | Plugin not installed or needs browser refresh | Hard refresh (Cmd+Shift+R / Ctrl+Shift+F5) |
|
||||
| **No StorageClasses listed** | Driver not installed or wrong provisioner | Verify `kubectl get sc` shows `tns.csi.io` provisioner |
|
||||
| **Driver status "Not installed"** | CSIDriver object missing | Check `kubectl get csidriver tns.csi.io` |
|
||||
| **Protocol/Pool/Server showing "—"** | StorageClass has no parameters | Inspect `kubectl get sc <name> -o yaml` |
|
||||
| **Metrics page empty** | Controller pod unreachable or no metrics port | Check controller pod logs and port 8080 |
|
||||
| **Snapshots tab empty** | Snapshot CRD not installed | Install `snapshot.storage.k8s.io` CRDs |
|
||||
| **Benchmark fails to start** | Missing RBAC for Jobs/PVCs | Add batch/jobs create+delete permissions |
|
||||
|
||||
## Development
|
||||
|
||||
**For detailed development guide, see [CONTRIBUTING.md](CONTRIBUTING.md).**
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/privilegedescalation/headlamp-tns-csi-plugin.git
|
||||
cd headlamp-tns-csi-plugin
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run with hot reload
|
||||
npm start # Opens Headlamp at http://localhost:4466
|
||||
|
||||
# Build for production
|
||||
npm run build # outputs dist/main.js
|
||||
npm run package # creates headlamp-tns-csi-plugin-<version>.tar.gz
|
||||
|
||||
# Run tests
|
||||
npm test # 67 unit tests
|
||||
npm run test:watch # watch mode
|
||||
|
||||
# Code quality
|
||||
npm run lint # eslint
|
||||
npm run tsc # type-check
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
index.tsx -- Entry point. Registers sidebar entries, routes,
|
||||
detail section injection, and plugin settings.
|
||||
api/
|
||||
k8s.ts -- TypeScript types and filtering helpers for tns-csi
|
||||
resources (StorageClass, PV, PVC, Pod, Snapshot).
|
||||
metrics.ts -- Prometheus text format parser; fetchControllerMetrics
|
||||
via ApiProxy from controller pod port 8080.
|
||||
kbench.ts -- kbench Job+PVC manifest builders, FIO log parser,
|
||||
BenchmarkState discriminated union, format helpers.
|
||||
TnsCsiDataContext.tsx -- React context provider; shared data fetch across
|
||||
all pages (StorageClasses, PVs, PVCs, pods, driver).
|
||||
components/
|
||||
OverviewPage.tsx -- Dashboard: driver health, storage summary,
|
||||
protocol distribution, non-Bound PVC alerts.
|
||||
StorageClassesPage.tsx -- StorageClass list + slide-in detail panel.
|
||||
VolumesPage.tsx -- PV list + slide-in detail panel.
|
||||
SnapshotsPage.tsx -- VolumeSnapshot list + slide-in detail panel.
|
||||
MetricsPage.tsx -- Prometheus metrics display cards.
|
||||
BenchmarkPage.tsx -- Interactive kbench runner (ONLY write operation).
|
||||
DriverStatusCard.tsx -- Driver health/status card component.
|
||||
PVCDetailSection.tsx -- TNS-CSI section injected into PVC detail views.
|
||||
vitest.config.mts -- Vitest configuration (jsdom environment).
|
||||
vitest.setup.ts -- localStorage shim for Node 22+.
|
||||
```
|
||||
|
||||
## Key Technical Details
|
||||
|
||||
### Provisioner
|
||||
|
||||
All resources are filtered to provisioner `tns.csi.io`. StorageClasses with any other provisioner are invisible to the plugin.
|
||||
|
||||
### Driver Component Labels
|
||||
|
||||
| Component | Label Selector |
|
||||
| --------- | -------------- |
|
||||
| Controller | `app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller` |
|
||||
| Node | `app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=node` |
|
||||
|
||||
### Metrics Endpoint
|
||||
|
||||
The plugin fetches Prometheus text format metrics from:
|
||||
|
||||
```
|
||||
GET /api/v1/namespaces/kube-system/pods/<controller-pod>/proxy/metrics
|
||||
```
|
||||
|
||||
Extracted metrics include `kubelet_volume_stats_*`, `csi_operations_seconds_*`, and any custom tns-csi metrics exposed on port `8080`.
|
||||
|
||||
### kbench Benchmarks
|
||||
|
||||
The Benchmark page creates resources labeled `app.kubernetes.io/managed-by=headlamp-tns-csi-plugin`. It uses the `yasker/kbench:latest` image and runs a configurable FIO test. Results are parsed from the Job's pod log into IOPS, bandwidth (MB/s), and latency (µs) cards.
|
||||
|
||||
## Releasing
|
||||
|
||||
Releases are automated via **GitHub Actions**. To cut a release:
|
||||
|
||||
```bash
|
||||
# 1. Update CHANGELOG.md with new version
|
||||
# 2. Trigger the release workflow from GitHub Actions UI:
|
||||
# Actions → Release → Run workflow → enter version X.Y.Z
|
||||
```
|
||||
|
||||
This triggers the **GitHub Actions** release workflow (`.github/workflows/release.yaml`):
|
||||
|
||||
1. Build the plugin in a `node:20` container
|
||||
2. Update `package.json` and `artifacthub-pkg.yml` with the new version
|
||||
3. Package a `.tar.gz` tarball
|
||||
4. Compute SHA256 checksum and update `artifacthub-pkg.yml`
|
||||
5. Commit, tag, and create a GitHub release with the tarball attached
|
||||
|
||||
ArtifactHub syncs within 30 minutes. The new version will appear in the Headlamp plugin catalog automatically.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for:
|
||||
|
||||
- Development workflow
|
||||
- Branching strategy (feature branches required for code changes)
|
||||
- Commit message conventions (Conventional Commits)
|
||||
- PR process and review checklist
|
||||
- Code style guidelines
|
||||
- Testing requirements
|
||||
|
||||
## Links
|
||||
|
||||
- **[GitHub Repository](https://github.com/privilegedescalation/headlamp-tns-csi-plugin)** — Source code, issues, releases
|
||||
- **[Artifact Hub](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin)** — Plugin catalog listing
|
||||
- **[Headlamp](https://headlamp.dev/)** — Kubernetes web UI
|
||||
- **[tns-csi driver](https://github.com/fenio/tns-csi)** — TrueNAS CSI driver
|
||||
- **[kbench](https://github.com/longhorn/kbench)** — Storage benchmark tool
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0 License](LICENSE) — see LICENSE file for details.
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
# Security Policy
|
||||
|
||||
## Overview
|
||||
|
||||
The Headlamp TNS-CSI Plugin is a visibility and benchmarking tool for the tns-csi Kubernetes CSI driver. Security considerations center on Kubernetes RBAC, network policies, and the limited write operations performed by the Benchmark page.
|
||||
|
||||
## Security Model
|
||||
|
||||
### Primarily Read-Only
|
||||
|
||||
The plugin is **read-only** for all pages except Benchmark:
|
||||
|
||||
- **No secrets access**: The plugin does not read or store Kubernetes Secrets
|
||||
- **No CRD installation**: No custom resource definitions or cluster-level modifications
|
||||
- **No PII**: CSI resource metadata (names, namespaces, parameters) does not contain personally identifiable information
|
||||
- **No external egress**: All API calls go through the Kubernetes API server proxy; no external network calls
|
||||
|
||||
### The Benchmark Exception
|
||||
|
||||
The Benchmark page creates and deletes a Kubernetes Job and PVC to run storage benchmarks. These resources are:
|
||||
|
||||
- Labeled `app.kubernetes.io/managed-by=headlamp-tns-csi-plugin` for identification
|
||||
- Created only in the namespace the user explicitly selects
|
||||
- Automatically deleted when the benchmark completes or is stopped
|
||||
- Using the `yasker/kbench:latest` image (a public, well-known benchmark tool)
|
||||
|
||||
Grant benchmark write permissions only to users who should be able to initiate storage tests.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User Browser
|
||||
↓ (HTTPS)
|
||||
Headlamp Pod
|
||||
↓ (in-cluster service account or user token)
|
||||
Kubernetes API Server
|
||||
↓ (list/watch StorageClasses, PVs, PVCs, pods, snapshots)
|
||||
↓ (pod proxy: controller pod port 8080 → Prometheus metrics)
|
||||
↓ (pod proxy: kbench pod → FIO log for benchmark results)
|
||||
Plugin Frontend (React)
|
||||
```
|
||||
|
||||
All communication uses Kubernetes authentication and authorization mechanisms. The plugin never stores credentials or bypasses RBAC.
|
||||
|
||||
## RBAC Requirements
|
||||
|
||||
### Minimal Read-Only Permissions
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: headlamp-tns-csi-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumes", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses", "csidrivers"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["snapshot.storage.k8s.io"]
|
||||
resources: ["volumesnapshots", "volumesnapshotclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log", "pods/proxy"]
|
||||
verbs: ["get"]
|
||||
# pods/proxy is used to fetch Prometheus metrics from the controller pod
|
||||
```
|
||||
|
||||
### Additional Permissions for Benchmark Page
|
||||
|
||||
```yaml
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["create", "delete"]
|
||||
```
|
||||
|
||||
### Binding Example
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: headlamp-tns-csi
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system # adjust to your Headlamp namespace
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: headlamp-tns-csi-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
### ⚠️ Security Best Practices
|
||||
|
||||
1. **Principle of Least Privilege**: Grant benchmark write permissions (`jobs create/delete`, `persistentvolumeclaims create/delete`) only to users who need them
|
||||
2. **Namespace Scoping for Benchmarks**: If possible, restrict benchmark Job/PVC permissions to a dedicated benchmark namespace using a namespaced Role rather than ClusterRole
|
||||
3. **Pod Proxy Scoping**: Scope `pods/proxy` access to `kube-system` only, or to pods matching the tns-csi controller label
|
||||
4. **Audit Logging**: Enable Kubernetes audit logging to track all API requests made through the plugin
|
||||
5. **Image Pinning**: Consider pinning `yasker/kbench:latest` to a specific digest in your environment for supply chain security
|
||||
|
||||
## Network Security
|
||||
|
||||
### Network Policies
|
||||
|
||||
If your cluster uses NetworkPolicies, ensure the Kubernetes API server can proxy requests to the tns-csi controller pod on port `8080`:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-api-server-to-tns-csi-controller
|
||||
namespace: kube-system
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: tns-csi-driver
|
||||
app.kubernetes.io/component: controller
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
```
|
||||
|
||||
The Kubernetes API server performs the pod proxy hop, so policies should permit the API server (not Headlamp directly) to reach the controller pod.
|
||||
|
||||
### TLS/HTTPS
|
||||
|
||||
- **External Access**: Always access Headlamp over HTTPS
|
||||
- **Internal Communication**: Headlamp to API server uses the service account token over the cluster's internal network
|
||||
- **Pod Proxy**: API server → tns-csi controller happens over HTTP within the cluster (port 8080)
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Service Account (Default)
|
||||
|
||||
Headlamp runs with a dedicated service account (`headlamp` in `kube-system`). All users share the same RBAC permissions.
|
||||
|
||||
**Security Considerations:**
|
||||
- All users have identical access to plugin functionality including Benchmark
|
||||
- Suitable for trusted internal environments
|
||||
- Simpler RBAC management
|
||||
|
||||
### OIDC Token Authentication
|
||||
|
||||
Headlamp can use per-user OIDC tokens. RBAC is enforced per-user, enabling fine-grained access control:
|
||||
|
||||
- Read-only users: bind only the reader ClusterRole
|
||||
- Benchmark users: bind the additional write permissions
|
||||
- Users without permissions see appropriate 403 errors
|
||||
|
||||
## Vulnerability Reporting
|
||||
|
||||
### Supported Versions
|
||||
|
||||
Security updates are applied to the latest release only.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| latest | ✅ |
|
||||
| < latest | ❌ |
|
||||
|
||||
### Reporting a Vulnerability
|
||||
|
||||
Report security vulnerabilities via:
|
||||
|
||||
1. **GitHub Security Advisories**: [Report a vulnerability](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/security/advisories/new)
|
||||
2. **GitHub Issues**: Open an issue and mark it "security" if advisories are unavailable
|
||||
|
||||
**Please do not** open public GitHub issues for security vulnerabilities before a fix is available.
|
||||
|
||||
**Response Timeline:**
|
||||
- **Acknowledgment**: Within 48 hours
|
||||
- **Initial Assessment**: Within 1 week
|
||||
- **Fix Timeline**: Critical: 1–2 weeks; High: 2–4 weeks; Medium/Low: next release cycle
|
||||
|
||||
## Dependency Security
|
||||
|
||||
The project uses:
|
||||
- **npm audit**: Runs automatically during `npm install`
|
||||
- **GitHub Dependabot**: Monitors dependencies and creates PRs for updates
|
||||
|
||||
Headlamp itself (`@kinvolk/headlamp-plugin`) is a peer dependency. Security updates to Headlamp should be applied by upgrading your Headlamp installation.
|
||||
|
||||
**Minimum supported Headlamp version**: v0.20.0
|
||||
|
||||
## Deployment Security Checklist
|
||||
|
||||
Before deploying to production:
|
||||
|
||||
- [ ] **RBAC configured**: ClusterRole and ClusterRoleBinding exist for Headlamp service account
|
||||
- [ ] **Benchmark permissions scoped**: Write permissions granted only to appropriate users/groups
|
||||
- [ ] **Network policies**: Allow API server → tns-csi controller traffic on port 8080
|
||||
- [ ] **TLS enabled**: Headlamp accessible only via HTTPS
|
||||
- [ ] **Audit logging enabled**: Kubernetes API audit logs capture requests
|
||||
- [ ] **Plugin version**: Running latest release
|
||||
- [ ] **Dependencies audited**: `npm audit` shows no critical vulnerabilities
|
||||
|
||||
## Compliance
|
||||
|
||||
### Data Residency
|
||||
|
||||
All data remains within your Kubernetes cluster. The plugin does not:
|
||||
- Send data to external services
|
||||
- Store data in browser localStorage (except any future settings)
|
||||
- Use third-party analytics or tracking
|
||||
|
||||
### Audit Trail
|
||||
|
||||
All API requests are logged in Kubernetes API audit logs (if enabled). Pod proxy requests to the controller pod's metrics endpoint appear as:
|
||||
|
||||
```json
|
||||
{
|
||||
"verb": "get",
|
||||
"requestURI": "/api/v1/namespaces/kube-system/pods/<controller-pod>/proxy/metrics",
|
||||
"user": {
|
||||
"username": "system:serviceaccount:kube-system:headlamp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Privacy
|
||||
|
||||
The plugin processes only technical metadata (resource names, namespaces, CSI parameters, metrics values). No personal data is collected, stored, or transmitted.
|
||||
|
||||
## Contact
|
||||
|
||||
- **Security Issues**: [GitHub Security Advisories](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/security/advisories)
|
||||
- **General Questions**: [GitHub Discussions](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/discussions)
|
||||
- **Bug Reports**: [GitHub Issues](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/issues)
|
||||
|
||||
## License
|
||||
|
||||
This plugin is provided under the Apache-2.0 License. See [LICENSE](LICENSE) for details.
|
||||
@@ -0,0 +1,57 @@
|
||||
version: "0.2.3"
|
||||
name: headlamp-tns-csi-plugin
|
||||
displayName: TrueNAS CSI (tns-csi)
|
||||
description: >-
|
||||
Headlamp plugin for tns-csi CSI driver visibility and kbench storage
|
||||
benchmarking. Surfaces StorageClasses with protocol/pool/server details,
|
||||
PersistentVolumes, VolumeSnapshots, Prometheus metrics from the controller
|
||||
pod, and an interactive kbench benchmark runner with FIO result cards.
|
||||
Supports NFS, NVMe-oF, and iSCSI protocols. Read-only except for the
|
||||
Benchmark page.
|
||||
createdAt: "2026-02-18T00:00:00Z"
|
||||
license: Apache-2.0
|
||||
category: storage
|
||||
|
||||
homeURL: https://github.com/privilegedescalation/headlamp-tns-csi-plugin
|
||||
appVersion: "0.1.0"
|
||||
|
||||
keywords:
|
||||
- headlamp
|
||||
- kubernetes
|
||||
- storage
|
||||
- csi
|
||||
- tns-csi
|
||||
- truenas
|
||||
- nfs
|
||||
- nvmeof
|
||||
- iscsi
|
||||
- kbench
|
||||
- benchmarking
|
||||
- prometheus
|
||||
|
||||
maintainers:
|
||||
- name: privilegedescalation
|
||||
email: chris@farhood.org
|
||||
|
||||
provider:
|
||||
name: privilegedescalation
|
||||
|
||||
links:
|
||||
- name: GitHub
|
||||
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin
|
||||
- name: Issues
|
||||
url: https://github.com/privilegedescalation/headlamp-tns-csi-plugin/issues
|
||||
- name: tns-csi driver
|
||||
url: https://github.com/fenio/tns-csi
|
||||
- name: kbench
|
||||
url: https://github.com/longhorn/kbench
|
||||
|
||||
changes:
|
||||
- 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.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"
|
||||
@@ -0,0 +1,6 @@
|
||||
# Artifact Hub repository metadata
|
||||
repositoryID: cae81660-2624-4e02-8ac2-a176cbe94402
|
||||
|
||||
owners:
|
||||
- name: privilegedescalation
|
||||
email: ""
|
||||
@@ -0,0 +1,73 @@
|
||||
# TNS-CSI Plugin Documentation
|
||||
|
||||
Welcome to the Headlamp TNS-CSI Plugin documentation.
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **[Quick Start](getting-started/quick-start.md)** — Get up and running in 5 minutes
|
||||
- **[Installation Guide](getting-started/installation.md)** — All installation methods
|
||||
- **[Troubleshooting](troubleshooting/README.md)** — Common issues and fixes
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Getting Started
|
||||
|
||||
| Guide | Description |
|
||||
| ----- | ----------- |
|
||||
| [Quick Start](getting-started/quick-start.md) | Fastest path to a working installation |
|
||||
| [Installation](getting-started/installation.md) | Plugin Manager, manual tarball, build from source |
|
||||
| [Prerequisites](getting-started/prerequisites.md) | Headlamp version, tns-csi driver, RBAC |
|
||||
|
||||
### User Guide
|
||||
|
||||
| Guide | Description |
|
||||
| ----- | ----------- |
|
||||
| [Overview Dashboard](user-guide/overview.md) | Driver health, storage summary, protocol distribution |
|
||||
| [Storage Classes](user-guide/storage-classes.md) | StorageClass list and detail panel |
|
||||
| [Volumes](user-guide/volumes.md) | PersistentVolume list and detail panel |
|
||||
| [Snapshots](user-guide/snapshots.md) | VolumeSnapshot list and CRD requirements |
|
||||
| [Metrics](user-guide/metrics.md) | Prometheus metrics display |
|
||||
| [Benchmark](user-guide/benchmark.md) | kbench interactive storage benchmarking |
|
||||
| [PVC Detail Injection](user-guide/pvc-detail.md) | TNS-CSI section in PVC detail views |
|
||||
| [RBAC Permissions](user-guide/rbac.md) | Required permissions per feature |
|
||||
|
||||
### Architecture
|
||||
|
||||
| Guide | Description |
|
||||
| ----- | ----------- |
|
||||
| [Overview](architecture/overview.md) | System architecture, data flow, component hierarchy |
|
||||
| [Data Flow](architecture/data-flow.md) | How data moves from K8s API to the UI |
|
||||
| [Design Decisions](architecture/design-decisions.md) | Key architectural choices and rationale |
|
||||
|
||||
### Deployment
|
||||
|
||||
| Guide | Description |
|
||||
| ----- | ----------- |
|
||||
| [Helm](deployment/helm.md) | Deploy with Helm (recommended) |
|
||||
| [Production Checklist](deployment/production.md) | Security and reliability checklist |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Guide | Description |
|
||||
| ----- | ----------- |
|
||||
| [Common Issues](troubleshooting/README.md) | Quick diagnosis table |
|
||||
| [RBAC Issues](troubleshooting/rbac.md) | 403 errors, missing permissions |
|
||||
| [Driver Detection](troubleshooting/driver.md) | Driver not installed, wrong provisioner |
|
||||
| [Metrics Issues](troubleshooting/metrics.md) | Empty metrics page, unreachable controller |
|
||||
| [Benchmark Issues](troubleshooting/benchmark.md) | Benchmark fails to start or complete |
|
||||
|
||||
### Development
|
||||
|
||||
| Guide | Description |
|
||||
| ----- | ----------- |
|
||||
| [Development Setup](development/setup.md) | Clone, install, run dev server |
|
||||
| [Testing](development/testing.md) | Unit tests, mocking headlamp APIs |
|
||||
| [Release Process](development/release.md) | How releases are cut and published |
|
||||
|
||||
## External Links
|
||||
|
||||
- **[GitHub Repository](https://github.com/privilegedescalation/headlamp-tns-csi-plugin)**
|
||||
- **[Artifact Hub](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin)**
|
||||
- **[tns-csi Driver](https://github.com/fenio/tns-csi)**
|
||||
- **[kbench](https://github.com/longhorn/kbench)**
|
||||
- **[Headlamp](https://headlamp.dev/)**
|
||||
@@ -0,0 +1,140 @@
|
||||
# Architecture Overview
|
||||
|
||||
## System Architecture
|
||||
|
||||
The TNS-CSI plugin is a single-page React application bundled as a Headlamp plugin. It runs entirely in the browser and communicates with Kubernetes exclusively through Headlamp's proxied API.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Browser │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ React Plugin Bundle │ │
|
||||
│ │ │ │
|
||||
│ │ index.tsx ── registerRoute/Sidebar/etc. │ │
|
||||
│ │ │ │
|
||||
│ │ TnsCsiDataProvider (React Context) │ │
|
||||
│ │ ├── K8s.ResourceClasses hooks (live watch) │ │
|
||||
│ │ └── ApiProxy.request (async fetch) │ │
|
||||
│ │ │ │
|
||||
│ │ Pages: │ │
|
||||
│ │ OverviewPage StorageClassesPage │ │
|
||||
│ │ VolumesPage SnapshotsPage │ │
|
||||
│ │ MetricsPage BenchmarkPage │ │
|
||||
│ │ │ │
|
||||
│ │ PVCDetailSection (injected into PVC views) │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└───────────────────────┬─────────────────────────────┘
|
||||
│ HTTPS
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Headlamp Pod (kube-system) │
|
||||
│ │
|
||||
│ Headlamp UI server + API proxy │
|
||||
│ (forwards requests using service account token │
|
||||
│ or user-supplied OIDC token) │
|
||||
└───────────────────────┬─────────────────────────────┘
|
||||
│ in-cluster
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Kubernetes API Server │
|
||||
│ │
|
||||
│ ├── /apis/storage.k8s.io/v1/storageclasses │
|
||||
│ ├── /api/v1/persistentvolumes │
|
||||
│ ├── /api/v1/persistentvolumeclaims │
|
||||
│ ├── /api/v1/namespaces/kube-system/pods │
|
||||
│ ├── /apis/storage.k8s.io/v1/csidrivers │
|
||||
│ ├── /apis/snapshot.storage.k8s.io/v1/... │
|
||||
│ ├── /api/v1/namespaces/kube-system/pods/<pod>/proxy/metrics
|
||||
│ └── (Benchmark) /apis/batch/v1/jobs │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```
|
||||
index.tsx
|
||||
└── TnsCsiDataProvider
|
||||
├── OverviewPage
|
||||
│ └── DriverStatusCard
|
||||
├── StorageClassesPage
|
||||
│ └── StorageClassDetailPanel (slide-in)
|
||||
├── VolumesPage
|
||||
│ └── VolumeDetailPanel (slide-in)
|
||||
├── SnapshotsPage
|
||||
│ └── SnapshotDetailPanel (slide-in)
|
||||
├── MetricsPage
|
||||
└── BenchmarkPage
|
||||
|
||||
registerDetailsViewSection
|
||||
└── TnsCsiDataProvider
|
||||
└── PVCDetailSection (injected)
|
||||
```
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Data | Source | Mechanism |
|
||||
| ---- | ------ | --------- |
|
||||
| StorageClasses | `storage.k8s.io/v1` | `K8s.ResourceClasses.StorageClass.useList()` — live watch |
|
||||
| PersistentVolumes | `core/v1` | `K8s.ResourceClasses.PersistentVolume.useList()` — live watch |
|
||||
| PersistentVolumeClaims | `core/v1` | `K8s.ResourceClasses.PersistentVolumeClaim.useList()` — live watch |
|
||||
| CSIDriver | `storage.k8s.io/v1` | `ApiProxy.request` — one-shot fetch |
|
||||
| Controller pods | `core/v1` | `ApiProxy.request` with label selector — one-shot fetch |
|
||||
| Node pods | `core/v1` | `ApiProxy.request` with label selector — one-shot fetch |
|
||||
| VolumeSnapshots | `snapshot.storage.k8s.io/v1` | `ApiProxy.request` — graceful degradation if CRD absent |
|
||||
| Prometheus metrics | Controller pod port 8080 | `ApiProxy.request` pod proxy |
|
||||
| kbench FIO logs | Benchmark Job pod | `ApiProxy.request` pod log |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### KubeObject jsonData Extraction
|
||||
|
||||
Headlamp's `useList()` hooks return KubeObject class instances, not plain JSON objects. The class only exposes getter-defined fields (`provisioner`, `reclaimPolicy`, `volumeBindingMode`, `allowVolumeExpansion` for StorageClass). All other fields — including `parameters`, `spec`, and `status` — must be accessed via `.jsonData`.
|
||||
|
||||
`TnsCsiDataContext.tsx` extracts `jsonData` from every item before passing to filter/type helpers:
|
||||
|
||||
```typescript
|
||||
const extractJsonData = (items: unknown[]): unknown[] =>
|
||||
items.map(item =>
|
||||
item && typeof item === 'object' && 'jsonData' in item
|
||||
? (item as { jsonData: unknown }).jsonData
|
||||
: item
|
||||
);
|
||||
```
|
||||
|
||||
This is the single most important architectural invariant to preserve when working with headlamp hook data.
|
||||
|
||||
### Context Provider Pattern
|
||||
|
||||
`TnsCsiDataProvider` wraps every route component. This ensures:
|
||||
- All data fetching happens once per page navigation (not once per component)
|
||||
- All pages share the same filtered StorageClasses, PVs, PVCs, and pod lists
|
||||
- The `refresh()` callback triggers a `refreshKey` increment which re-runs async fetches
|
||||
|
||||
### Read-Only Constraint
|
||||
|
||||
The only write operation in the entire plugin is `BenchmarkPage.tsx`, which creates and deletes a Kubernetes Job and PVC. All other pages are strictly read-only. This is intentional and should be preserved.
|
||||
|
||||
### Detail Panel Pattern
|
||||
|
||||
Slide-in detail panels use URL hash state (`location.hash`) so:
|
||||
- Panel state survives browser refresh
|
||||
- Back button closes the panel
|
||||
- Deep-linking to a specific resource is possible
|
||||
|
||||
Pattern: `history.push(\`\${location.pathname}#\${name}\`)` to open, `history.push(location.pathname)` to close.
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
The snapshot CRD (`snapshot.storage.k8s.io/v1`) may not be installed. The context provider catches the 404/405 error and sets `snapshotCrdAvailable: false`. The Snapshots page shows an informational message instead of an error. Prometheus metrics similarly fall back to placeholder cards.
|
||||
|
||||
## Module Responsibilities
|
||||
|
||||
| File | Responsibility |
|
||||
| ---- | -------------- |
|
||||
| `src/index.tsx` | All registrations — sidebar entries, routes, detail section, plugin settings |
|
||||
| `src/api/k8s.ts` | Type definitions, type guards, filter helpers, format utilities |
|
||||
| `src/api/metrics.ts` | Prometheus text format parser, `fetchControllerMetrics` |
|
||||
| `src/api/kbench.ts` | kbench manifest builders, FIO log parser, `BenchmarkState` discriminated union |
|
||||
| `src/api/TnsCsiDataContext.tsx` | Shared data fetching and filtering; the `extractJsonData` pattern |
|
||||
| `src/components/*.tsx` | Page and panel UI components |
|
||||
@@ -0,0 +1,154 @@
|
||||
# Deployment with Helm
|
||||
|
||||
## Basic Helm Installation
|
||||
|
||||
Add the Headlamp Helm repository and deploy with the plugin configured:
|
||||
|
||||
```bash
|
||||
helm repo add headlamp https://headlamp-k8s.github.io/headlamp/
|
||||
helm repo update
|
||||
|
||||
helm install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
--create-namespace \
|
||||
--set config.pluginsDir=/headlamp/plugins \
|
||||
--set pluginsManager.sources[0].name=headlamp-tns-csi-plugin \
|
||||
--set pluginsManager.sources[0].url=https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.1.0/headlamp-tns-csi-plugin-0.1.0.tar.gz
|
||||
```
|
||||
|
||||
## Complete values.yaml Example
|
||||
|
||||
```yaml
|
||||
# headlamp-values.yaml
|
||||
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
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
|
||||
|
||||
serviceAccount:
|
||||
name: headlamp
|
||||
|
||||
# Optional: OIDC authentication
|
||||
# oidcConfig:
|
||||
# clientID: headlamp
|
||||
# clientSecret: <your-secret>
|
||||
# issuerURL: https://your-oidc-provider.example.com/
|
||||
# scopes: "openid profile email groups"
|
||||
```
|
||||
|
||||
Apply:
|
||||
|
||||
```bash
|
||||
helm install headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
-f headlamp-values.yaml
|
||||
```
|
||||
|
||||
## FluxCD HelmRelease
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
interval: 12h
|
||||
url: https://headlamp-k8s.github.io/headlamp/
|
||||
---
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
interval: 1h
|
||||
chart:
|
||||
spec:
|
||||
chart: headlamp
|
||||
version: ">=0.26.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
values:
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
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
|
||||
```
|
||||
|
||||
## RBAC Manifest (Apply Separately)
|
||||
|
||||
After deploying Headlamp, apply the plugin's RBAC:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: headlamp-tns-csi-reader
|
||||
rules:
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses", "csidrivers"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumes", "persistentvolumeclaims", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log", "pods/proxy"]
|
||||
verbs: ["get"]
|
||||
- apiGroups: ["snapshot.storage.k8s.io"]
|
||||
resources: ["volumesnapshots", "volumesnapshotclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Uncomment for Benchmark page:
|
||||
# - apiGroups: ["batch"]
|
||||
# resources: ["jobs"]
|
||||
# verbs: ["get", "list", "watch", "create", "delete"]
|
||||
# - apiGroups: [""]
|
||||
# resources: ["persistentvolumeclaims"]
|
||||
# verbs: ["create", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: headlamp-tns-csi
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: headlamp-tns-csi-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
## Upgrading the Plugin
|
||||
|
||||
To upgrade to a new plugin version, update the `url` in your values and apply:
|
||||
|
||||
```bash
|
||||
helm upgrade headlamp headlamp/headlamp \
|
||||
--namespace kube-system \
|
||||
-f headlamp-values.yaml
|
||||
```
|
||||
|
||||
Or update the FluxCD HelmRelease and let Flux reconcile.
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Headlamp v0.20+ deployed
|
||||
- [ ] Plugin installed and sidebar entry visible
|
||||
- [ ] RBAC ClusterRole and ClusterRoleBinding applied
|
||||
- [ ] tns-csi driver installed in `kube-system` with standard labels
|
||||
- [ ] Controller pod exposes port 8080 for Prometheus metrics
|
||||
- [ ] Headlamp accessible via HTTPS
|
||||
- [ ] (Optional) Snapshot CRD installed for Snapshots tab
|
||||
- [ ] (Optional) Benchmark namespace created and write RBAC applied
|
||||
@@ -0,0 +1,149 @@
|
||||
# Testing Guide
|
||||
|
||||
## Test Suite Overview
|
||||
|
||||
The plugin has 67 unit tests across 4 test files:
|
||||
|
||||
| File | Tests | Coverage |
|
||||
| ---- | ----- | -------- |
|
||||
| `src/api/k8s.test.ts` | Type guards, filter helpers, format utilities | k8s.ts |
|
||||
| `src/api/metrics.test.ts` | Prometheus text format parser | metrics.ts |
|
||||
| `src/api/kbench.test.ts` | FIO log parser, manifest builders, format helpers | kbench.ts |
|
||||
| `src/api/TnsCsiDataContext.test.tsx` | Context provider integration | TnsCsiDataContext.tsx |
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests once
|
||||
npm test
|
||||
|
||||
# Watch mode (re-runs on file changes)
|
||||
npm run test:watch
|
||||
|
||||
# TypeScript type-check (no emit)
|
||||
npm run tsc
|
||||
```
|
||||
|
||||
All tests must pass before committing. The CI workflow enforces this.
|
||||
|
||||
## Test Framework
|
||||
|
||||
- **Vitest** — test runner
|
||||
- **@testing-library/react** — React component testing utilities
|
||||
- **jsdom** — DOM environment (configured in `vitest.config.mts`)
|
||||
|
||||
## Mocking Headlamp APIs
|
||||
|
||||
Headlamp APIs must be mocked in tests. Use this pattern:
|
||||
|
||||
```typescript
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
ApiProxy: {
|
||||
request: vi.fn().mockResolvedValue({ items: [] }),
|
||||
},
|
||||
K8s: {
|
||||
ResourceClasses: {
|
||||
StorageClass: {
|
||||
useList: vi.fn(() => [[], null]),
|
||||
},
|
||||
PersistentVolume: {
|
||||
useList: vi.fn(() => [[], null]),
|
||||
},
|
||||
PersistentVolumeClaim: {
|
||||
useList: vi.fn(() => [[], null]),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
SimpleTable: ({ data, emptyMessage }: { data: unknown[]; emptyMessage: string }) =>
|
||||
data.length === 0 ? <p>{emptyMessage}</p> : <table />,
|
||||
Loader: ({ title }: { title: string }) => <div>{title}</div>,
|
||||
// add other CommonComponents as needed
|
||||
}));
|
||||
```
|
||||
|
||||
## Testing the Prometheus Parser
|
||||
|
||||
```typescript
|
||||
import { parsePrometheusText, extractTnsCsiMetrics } from '../metrics';
|
||||
|
||||
it('parses gauge metrics correctly', () => {
|
||||
const text = `
|
||||
# HELP kubelet_volume_stats_capacity_bytes Capacity in bytes of the volume
|
||||
# TYPE kubelet_volume_stats_capacity_bytes gauge
|
||||
kubelet_volume_stats_capacity_bytes{namespace="default",persistentvolumeclaim="my-pvc"} 10737418240
|
||||
`;
|
||||
const metrics = parsePrometheusText(text);
|
||||
expect(metrics.get('kubelet_volume_stats_capacity_bytes{namespace="default",persistentvolumeclaim="my-pvc"}')).toBe(10737418240);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing the FIO Log Parser
|
||||
|
||||
```typescript
|
||||
import { parseKbenchLog } from '../kbench';
|
||||
|
||||
it('parses kbench FIO output into result cards', () => {
|
||||
const log = `
|
||||
READ: bw=512MiB/s (537MB/s), 512MiB/s-512MiB/s (537MB/s-537MB/s), io=32.0GiB (34.4GB), run=63999-63999msec
|
||||
iops : min=128000, max=135000, avg=131072.00, stdev=1024.00, samples=64
|
||||
lat (usec) : min=10, max=500, avg=50.00, stdev=20.00
|
||||
`;
|
||||
const result = parseKbenchLog(log);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.readBandwidthMBs).toBeGreaterThan(0);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Type Guards
|
||||
|
||||
```typescript
|
||||
import { isTnsCsiStorageClass } from '../k8s';
|
||||
|
||||
it('identifies tns.csi.io provisioner', () => {
|
||||
expect(isTnsCsiStorageClass({ provisioner: 'tns.csi.io', metadata: { name: 'test' } })).toBe(true);
|
||||
expect(isTnsCsiStorageClass({ provisioner: 'other.csi.io', metadata: { name: 'test' } })).toBe(false);
|
||||
expect(isTnsCsiStorageClass(null)).toBe(false);
|
||||
expect(isTnsCsiStorageClass(undefined)).toBe(false);
|
||||
});
|
||||
```
|
||||
|
||||
## vitest.setup.ts
|
||||
|
||||
The setup file shims `localStorage` for Node 22+ (jsdom doesn't provide it in some versions):
|
||||
|
||||
```typescript
|
||||
// vitest.setup.ts
|
||||
if (typeof localStorage === 'undefined') {
|
||||
const store: Record<string, string> = {};
|
||||
Object.defineProperty(global, 'localStorage', {
|
||||
value: {
|
||||
getItem: (k: string) => store[k] ?? null,
|
||||
setItem: (k: string, v: string) => { store[k] = v; },
|
||||
removeItem: (k: string) => { delete store[k]; },
|
||||
clear: () => { Object.keys(store).forEach(k => delete store[k]); },
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## CI Test Enforcement
|
||||
|
||||
The GitHub Actions CI workflow runs tests on every push and pull request:
|
||||
|
||||
```yaml
|
||||
- name: Run unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Type-check
|
||||
run: npx tsc --noEmit
|
||||
```
|
||||
|
||||
Both must pass for the PR to merge.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Installation Guide
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Method 1: Headlamp Plugin Manager (Recommended)
|
||||
|
||||
The plugin is published on [Artifact Hub](https://artifacthub.io/packages/headlamp/headlamp-tns-csi-plugin/headlamp-tns-csi-plugin).
|
||||
|
||||
**Via Headlamp UI:**
|
||||
|
||||
1. Navigate to **Settings → Plugins → Catalog**
|
||||
2. Search for "TNS CSI" or "TrueNAS"
|
||||
3. Click **Install**
|
||||
4. Refresh the page
|
||||
|
||||
**Via Helm values:**
|
||||
|
||||
```yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
**Via FluxCD HelmRelease:**
|
||||
|
||||
```yaml
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: headlamp
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: headlamp
|
||||
values:
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
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
|
||||
```
|
||||
|
||||
### Method 2: Manual Tarball Install
|
||||
|
||||
Download and extract the plugin directly:
|
||||
|
||||
```bash
|
||||
# Download the release tarball
|
||||
wget https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.1.0/headlamp-tns-csi-plugin-0.1.0.tar.gz
|
||||
|
||||
# Verify the checksum
|
||||
echo "14a3e8c13d0b894a41aa1cfccbcb1f6af09dcbb8fd95c7040a540987ea2096a7 headlamp-tns-csi-plugin-0.1.0.tar.gz" | sha256sum --check
|
||||
|
||||
# Extract into your Headlamp plugins directory
|
||||
tar xzf headlamp-tns-csi-plugin-0.1.0.tar.gz -C /headlamp/plugins/
|
||||
```
|
||||
|
||||
The plugin directory should appear as `/headlamp/plugins/headlamp-tns-csi-plugin/`.
|
||||
|
||||
Restart Headlamp (or the pod) after extracting.
|
||||
|
||||
### Method 3: Sidecar Container
|
||||
|
||||
For Headlamp deployments where you prefer managing plugins as container init sidecars:
|
||||
|
||||
```yaml
|
||||
initContainers:
|
||||
- name: install-tns-csi-plugin
|
||||
image: alpine:3
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
wget -O /tmp/plugin.tar.gz \
|
||||
https://github.com/privilegedescalation/headlamp-tns-csi-plugin/releases/download/v0.1.0/headlamp-tns-csi-plugin-0.1.0.tar.gz
|
||||
tar xzf /tmp/plugin.tar.gz -C /headlamp/plugins/
|
||||
volumeMounts:
|
||||
- name: plugins
|
||||
mountPath: /headlamp/plugins
|
||||
```
|
||||
|
||||
### Method 4: Build from Source
|
||||
|
||||
For development or to test unreleased changes:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/privilegedescalation/headlamp-tns-csi-plugin.git
|
||||
cd headlamp-tns-csi-plugin
|
||||
npm install
|
||||
npm run build
|
||||
npm run package
|
||||
# Produces headlamp-tns-csi-plugin-0.1.0.tar.gz
|
||||
|
||||
# Extract to your Headlamp plugins directory
|
||||
tar xzf headlamp-tns-csi-plugin-0.1.0.tar.gz -C /headlamp/plugins/
|
||||
```
|
||||
|
||||
Or use `headlamp-plugin extract` for automatic placement:
|
||||
|
||||
```bash
|
||||
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||
```
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After installing the plugin:
|
||||
|
||||
1. **Configure RBAC** — see [RBAC Permissions](../user-guide/rbac.md)
|
||||
2. **Verify the plugin loads** — refresh browser and look for "TrueNAS (tns-csi)" in the sidebar
|
||||
3. **Check the Overview page** — driver health card should show tns-csi status
|
||||
|
||||
## Upgrading
|
||||
|
||||
To upgrade to a new version, repeat the installation method you used. The new tarball replaces the old plugin directory.
|
||||
|
||||
For Plugin Manager installs, the catalog will show available updates.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
Remove the plugin directory from your Headlamp plugins directory:
|
||||
|
||||
```bash
|
||||
rm -rf /headlamp/plugins/headlamp-tns-csi-plugin/
|
||||
```
|
||||
|
||||
Or via the Headlamp UI: **Settings → Plugins → headlamp-tns-csi-plugin → Uninstall**.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Quick Start
|
||||
|
||||
Get the TNS-CSI plugin running in Headlamp in about 5 minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Headlamp v0.20+ running in your cluster
|
||||
- tns-csi driver installed in `kube-system`
|
||||
- `kubectl` access to your cluster
|
||||
|
||||
## Step 1: Install the Plugin
|
||||
|
||||
### Via Headlamp UI (Easiest)
|
||||
|
||||
1. Open Headlamp and navigate to **Settings → Plugins → Catalog**
|
||||
2. Search for **"TNS CSI"** or **"TrueNAS"**
|
||||
3. Click **Install**
|
||||
4. Refresh the browser
|
||||
|
||||
### Via Helm
|
||||
|
||||
Add the plugin source to your Headlamp Helm values:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
pluginsDir: /headlamp/plugins
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Then upgrade your Headlamp release:
|
||||
|
||||
```bash
|
||||
helm upgrade headlamp headlamp/headlamp -f values.yaml -n kube-system
|
||||
```
|
||||
|
||||
## Step 2: Configure RBAC
|
||||
|
||||
The plugin needs read access to storage resources and the tns-csi controller pod's metrics endpoint.
|
||||
|
||||
Apply the minimal RBAC:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: headlamp-tns-csi-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumes", "persistentvolumeclaims", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses", "csidrivers"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["snapshot.storage.k8s.io"]
|
||||
resources: ["volumesnapshots", "volumesnapshotclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log", "pods/proxy"]
|
||||
verbs: ["get"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: headlamp-tns-csi
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: headlamp-tns-csi-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
Adjust `name: headlamp` and `namespace: kube-system` to match your Headlamp service account.
|
||||
|
||||
## Step 3: Verify
|
||||
|
||||
1. Open Headlamp — you should see **TrueNAS (tns-csi)** in the left sidebar
|
||||
2. Click **Overview** — you should see the driver health card and storage summary
|
||||
3. Click **Storage Classes** — your tns-csi StorageClasses should appear with Protocol, Pool, and Server filled in
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
| ------- | --- |
|
||||
| No sidebar entry | Hard-refresh browser (Cmd+Shift+R) |
|
||||
| Driver shows "Not installed" | Run `kubectl get csidriver tns.csi.io` |
|
||||
| StorageClasses empty | Check `kubectl get sc` for `tns.csi.io` provisioner |
|
||||
| Protocol/Pool/Server show "—" | Check `kubectl get sc <name> -o yaml` for `.parameters` |
|
||||
| Metrics page empty | Verify controller pod exposes port 8080 |
|
||||
|
||||
For more detail see [Troubleshooting](../troubleshooting/README.md).
|
||||
@@ -0,0 +1,112 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Quick Diagnosis
|
||||
|
||||
| Symptom | Likely Cause | Fix |
|
||||
| ------- | ------------ | --- |
|
||||
| **Plugin not in sidebar** | Not installed or browser cache | Hard refresh (Cmd+Shift+R / Ctrl+Shift+F5) |
|
||||
| **"TrueNAS (tns-csi)" missing from sidebar** | Plugin not loaded | Check Headlamp plugin manager or restart Headlamp pod |
|
||||
| **No StorageClasses listed** | Wrong provisioner or driver not installed | See [Driver Detection](#driver-detection) |
|
||||
| **Driver status "Not installed"** | CSIDriver object missing | `kubectl get csidriver tns.csi.io` |
|
||||
| **Protocol/Pool/Server showing "—"** | StorageClass missing parameters | `kubectl get sc <name> -o yaml` to inspect |
|
||||
| **403 on any page** | Missing RBAC | See [RBAC Issues](rbac.md) |
|
||||
| **Metrics page empty** | Controller pod unreachable or no metrics | See [Metrics Issues](metrics.md) |
|
||||
| **Snapshots tab: "CRD not available"** | Snapshot CRD not installed | Install `snapshot.storage.k8s.io` CRDs |
|
||||
| **Snapshots tab empty (no message)** | No snapshots or wrong snapshot class | Check VolumeSnapshotClass driver field |
|
||||
| **Benchmark fails immediately** | Missing RBAC for Jobs/PVCs | See [Benchmark Issues](benchmark.md) |
|
||||
| **Benchmark stuck in "Running"** | kbench pod not starting | `kubectl get pods -n <ns> -l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin` |
|
||||
| **Page loads but data is stale** | Watch connection dropped | Click the Refresh button or reload the page |
|
||||
|
||||
## Driver Detection
|
||||
|
||||
The plugin detects the tns-csi driver by querying:
|
||||
|
||||
```
|
||||
GET /apis/storage.k8s.io/v1/csidrivers/tns.csi.io
|
||||
```
|
||||
|
||||
If this returns 404, the driver shows as "Not installed".
|
||||
|
||||
**Check:**
|
||||
|
||||
```bash
|
||||
kubectl get csidriver tns.csi.io
|
||||
```
|
||||
|
||||
If missing, verify the tns-csi driver is deployed. The driver registers its CSIDriver object on startup.
|
||||
|
||||
## StorageClass Parameters Showing "—"
|
||||
|
||||
StorageClass Protocol, Pool, and Server come from the StorageClass `parameters` field.
|
||||
|
||||
**Check:**
|
||||
|
||||
```bash
|
||||
kubectl get sc -o yaml | grep -A5 "provisioner: tns.csi.io"
|
||||
```
|
||||
|
||||
Expected output includes:
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
protocol: nfs
|
||||
pool: tank/k8s
|
||||
server: 192.168.1.1
|
||||
```
|
||||
|
||||
If `parameters` is absent, the StorageClass was created without them — the CSI driver documentation specifies the required parameters for each protocol.
|
||||
|
||||
## Controller Pods Not Showing
|
||||
|
||||
The Overview page shows controller and node pod counts using label selectors:
|
||||
|
||||
- Controller: `app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller`
|
||||
- Node: `app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=node`
|
||||
|
||||
**Check:**
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system -l app.kubernetes.io/name=tns-csi-driver
|
||||
```
|
||||
|
||||
If pods exist but aren't showing, verify the `app.kubernetes.io/component` label is set correctly.
|
||||
|
||||
## Infinite Loading Spinner
|
||||
|
||||
If a page shows a loading spinner indefinitely:
|
||||
|
||||
1. **Check browser console** for errors (F12 → Console)
|
||||
2. **Check network tab** for failed API requests (look for 403, 404, 500)
|
||||
3. **Check Headlamp pod logs**: `kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp`
|
||||
4. **Try refreshing** — the watch connection may have been interrupted
|
||||
|
||||
## Common API Errors
|
||||
|
||||
| HTTP Status | Meaning | Action |
|
||||
| ----------- | ------- | ------ |
|
||||
| `401 Unauthorized` | Token expired or invalid | Re-authenticate in Headlamp |
|
||||
| `403 Forbidden` | Missing RBAC permission | See [RBAC Issues](rbac.md) |
|
||||
| `404 Not Found` | Resource doesn't exist | Expected for optional resources (CSIDriver, snapshot CRD) |
|
||||
| `503 Service Unavailable` | API server overloaded | Wait and retry |
|
||||
|
||||
## Getting More Information
|
||||
|
||||
**Browser console:**
|
||||
|
||||
```
|
||||
F12 → Console tab
|
||||
```
|
||||
|
||||
Look for errors related to `tns-csi`, `headlamp-plugin`, or Kubernetes API paths.
|
||||
|
||||
**Headlamp pod logs:**
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp --tail=100
|
||||
```
|
||||
|
||||
**tns-csi controller logs:**
|
||||
|
||||
```bash
|
||||
kubectl logs -n kube-system -l app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller --tail=100
|
||||
```
|
||||
@@ -0,0 +1,93 @@
|
||||
# Benchmark Issues
|
||||
|
||||
## Benchmark Fails to Start
|
||||
|
||||
### Check RBAC
|
||||
|
||||
The Benchmark page requires permissions to create and delete Jobs and PVCs:
|
||||
|
||||
```bash
|
||||
kubectl auth can-i create jobs -n <benchmark-namespace> \
|
||||
--as=system:serviceaccount:kube-system:headlamp
|
||||
|
||||
kubectl auth can-i create persistentvolumeclaims -n <benchmark-namespace> \
|
||||
--as=system:serviceaccount:kube-system:headlamp
|
||||
```
|
||||
|
||||
Apply the additional permissions if missing — see [RBAC Issues](rbac.md) or [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
### Check the Target Namespace Exists
|
||||
|
||||
The namespace you select in the Benchmark form must exist. Create it if needed:
|
||||
|
||||
```bash
|
||||
kubectl create namespace <benchmark-namespace>
|
||||
```
|
||||
|
||||
## Benchmark Stuck in "Running"
|
||||
|
||||
### Check the kbench Pod
|
||||
|
||||
```bash
|
||||
kubectl get pods -n <benchmark-namespace> \
|
||||
-l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin
|
||||
```
|
||||
|
||||
Common states:
|
||||
|
||||
| Pod State | Cause | Action |
|
||||
| --------- | ----- | ------ |
|
||||
| `Pending` | PVC not provisioned or scheduler issue | Check PVC status and StorageClass |
|
||||
| `Init:Error` | kbench image pull failure | Check image pull policy and network |
|
||||
| `Running` | Benchmark in progress | Wait for completion |
|
||||
| `Completed` | Finished — results should appear | Check FIO log section |
|
||||
| `Error` / `OOMKilled` | kbench ran out of memory | Reduce test size or capacity |
|
||||
|
||||
### Check the PVC
|
||||
|
||||
```bash
|
||||
kubectl get pvc -n <benchmark-namespace> \
|
||||
-l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin
|
||||
```
|
||||
|
||||
If the PVC is stuck in `Pending`, the StorageClass provisioner may not be able to create the volume:
|
||||
|
||||
```bash
|
||||
kubectl describe pvc -n <benchmark-namespace> <pvc-name>
|
||||
```
|
||||
|
||||
Look for events at the bottom of the describe output.
|
||||
|
||||
### View kbench Logs Directly
|
||||
|
||||
```bash
|
||||
kubectl logs -n <benchmark-namespace> \
|
||||
-l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin \
|
||||
--tail=100
|
||||
```
|
||||
|
||||
## Leftover Resources After Failed Benchmark
|
||||
|
||||
If the benchmark was stopped or the plugin page was closed during a run, the Job and PVC may not have been cleaned up:
|
||||
|
||||
```bash
|
||||
# List leftover resources
|
||||
kubectl get jobs,pvc -n <benchmark-namespace> \
|
||||
-l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin
|
||||
|
||||
# Clean up manually
|
||||
kubectl delete jobs,pvc -n <benchmark-namespace> \
|
||||
-l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin
|
||||
```
|
||||
|
||||
The plugin adds the `app.kubernetes.io/managed-by=headlamp-tns-csi-plugin` label to all benchmark resources precisely to enable safe cleanup with this label selector.
|
||||
|
||||
## No Results Shown After Benchmark Completes
|
||||
|
||||
The plugin parses the FIO log output from the kbench pod. If results don't appear:
|
||||
|
||||
1. Check the pod completed successfully (status `Completed`, exit code 0)
|
||||
2. View the raw log: `kubectl logs -n <ns> <kbench-pod>`
|
||||
3. Look for the FIO result section — it should contain lines like `READ: bw=...` or `WRITE: bw=...`
|
||||
|
||||
If the kbench version produces output in a different format, the FIO log parser may not recognize it. Open a [GitHub Issue](https://github.com/privilegedescalation/headlamp-tns-csi-plugin/issues) with a sample of the log output.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Metrics Issues
|
||||
|
||||
## Metrics Page Shows No Data
|
||||
|
||||
### 1. Check the Controller Pod Is Running
|
||||
|
||||
```bash
|
||||
kubectl get pods -n kube-system \
|
||||
-l app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller
|
||||
```
|
||||
|
||||
The controller pod must be in `Running` state with all containers ready.
|
||||
|
||||
### 2. Verify Port 8080 Is Exposed
|
||||
|
||||
```bash
|
||||
# Check the pod spec for port 8080
|
||||
kubectl get pod -n kube-system <controller-pod-name> -o yaml | grep -A5 "ports:"
|
||||
```
|
||||
|
||||
If port 8080 is not declared, the tns-csi driver version you're running may not expose Prometheus metrics. Check the driver documentation.
|
||||
|
||||
### 3. Test the Metrics Endpoint Directly
|
||||
|
||||
```bash
|
||||
# Port-forward the controller pod
|
||||
kubectl port-forward -n kube-system \
|
||||
$(kubectl get pods -n kube-system -l app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller -o name | head -1) \
|
||||
8080:8080
|
||||
|
||||
# In another terminal
|
||||
curl http://localhost:8080/metrics | head -20
|
||||
```
|
||||
|
||||
If this returns Prometheus text format output, the endpoint is working. If it returns 404 or connection refused, the controller isn't exposing metrics.
|
||||
|
||||
### 4. Check RBAC for Pod Proxy
|
||||
|
||||
The plugin accesses metrics via the Kubernetes pod proxy sub-resource:
|
||||
|
||||
```
|
||||
GET /api/v1/namespaces/kube-system/pods/<pod>/proxy/metrics
|
||||
```
|
||||
|
||||
This requires `get` on `pods/proxy` in `kube-system`:
|
||||
|
||||
```bash
|
||||
kubectl auth can-i get pods/proxy \
|
||||
-n kube-system \
|
||||
--as=system:serviceaccount:kube-system:headlamp
|
||||
```
|
||||
|
||||
### 5. Network Policies
|
||||
|
||||
If `kube-system` has NetworkPolicies, ensure the Kubernetes API server can reach the controller pod on port 8080. The pod proxy hop is performed by the API server, not by Headlamp directly.
|
||||
|
||||
## Metrics Show Stale Values
|
||||
|
||||
The Metrics page fetches data on-demand when the page loads. Click **Refresh** to re-fetch the latest metrics from the controller pod.
|
||||
|
||||
## Some Metric Cards Show "—"
|
||||
|
||||
Not all tns-csi driver versions expose all metrics. The plugin shows placeholder "—" values for metrics that are absent from the Prometheus output. This is expected behavior.
|
||||
|
||||
The plugin specifically looks for:
|
||||
- `kubelet_volume_stats_*` metrics (volume I/O)
|
||||
- `csi_operations_seconds_*` metrics (CSI operation latency)
|
||||
- Any tns-csi specific metrics on port 8080
|
||||
@@ -0,0 +1,64 @@
|
||||
# RBAC Issues
|
||||
|
||||
## 403 Forbidden Errors
|
||||
|
||||
A 403 error means the identity making the API request (Headlamp's service account or the logged-in user's token) lacks the required permission.
|
||||
|
||||
### Diagnosing Which Permission Is Missing
|
||||
|
||||
Use `kubectl auth can-i` to check specific permissions:
|
||||
|
||||
```bash
|
||||
# Check if the Headlamp service account can list StorageClasses
|
||||
kubectl auth can-i list storageclasses \
|
||||
--as=system:serviceaccount:kube-system:headlamp
|
||||
|
||||
# Check pod proxy access (for metrics)
|
||||
kubectl auth can-i get pods/proxy \
|
||||
-n kube-system \
|
||||
--as=system:serviceaccount:kube-system:headlamp
|
||||
|
||||
# Check snapshot access
|
||||
kubectl auth can-i list volumesnapshots \
|
||||
--as=system:serviceaccount:kube-system:headlamp
|
||||
```
|
||||
|
||||
### Applying the Required RBAC
|
||||
|
||||
See [RBAC Permissions](../user-guide/rbac.md) for the complete ClusterRole manifest.
|
||||
|
||||
Quick apply:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/privilegedescalation/headlamp-tns-csi-plugin/main/docs/user-guide/rbac-manifest.yaml
|
||||
```
|
||||
|
||||
Or manually apply the ClusterRole and ClusterRoleBinding from [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
### OIDC Token Mode
|
||||
|
||||
If Headlamp is configured for OIDC authentication, each user's own token is used for API requests. The RBAC must be bound to the user's identity (email, group) rather than the service account:
|
||||
|
||||
```yaml
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: "engineering"
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Users not in the group will see 403 errors in the plugin.
|
||||
|
||||
### Benchmark 403
|
||||
|
||||
The Benchmark page requires additional write permissions:
|
||||
|
||||
```yaml
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["create", "delete"]
|
||||
```
|
||||
|
||||
If only the Benchmark page shows 403, add these rules to your ClusterRole (or a separate Role scoped to the benchmark namespace).
|
||||
@@ -0,0 +1,79 @@
|
||||
# Benchmark Page
|
||||
|
||||
The Benchmark page provides an interactive storage benchmark runner using [kbench](https://github.com/longhorn/kbench) (the Longhorn storage benchmark tool based on FIO).
|
||||
|
||||
## What It Does
|
||||
|
||||
1. You select a tns-csi StorageClass, a namespace, a PVC capacity, and an access mode
|
||||
2. The plugin creates a PVC and a Kubernetes Job that runs `yasker/kbench:latest`
|
||||
3. FIO log output streams in real-time from the kbench pod
|
||||
4. When complete, results are parsed and displayed as IOPS, bandwidth (MB/s), and latency (µs) cards
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- RBAC permissions for Jobs and PVCs — see [RBAC Permissions](rbac.md)
|
||||
- The target namespace must exist
|
||||
- The selected StorageClass must support the chosen access mode
|
||||
|
||||
## Running a Benchmark
|
||||
|
||||
1. Navigate to **TrueNAS (tns-csi) → Benchmark**
|
||||
2. Select a StorageClass from the dropdown (only tns-csi classes are listed)
|
||||
3. Enter the target namespace (defaults to `default`)
|
||||
4. Set PVC capacity (e.g., `10Gi`)
|
||||
5. Choose access mode (`ReadWriteOnce`, `ReadWriteMany`, etc.)
|
||||
6. Click **Run Benchmark**
|
||||
|
||||
The benchmark progress shows:
|
||||
- Benchmark state (Starting, Running, Parsing Results, Complete, Failed)
|
||||
- Live FIO log output as it streams from the pod
|
||||
- Result cards once FIO completes
|
||||
|
||||
## Result Cards
|
||||
|
||||
When the benchmark completes, the plugin displays:
|
||||
|
||||
| Card | Metric |
|
||||
| ---- | ------ |
|
||||
| Read IOPS | Random 4K read I/O operations per second |
|
||||
| Write IOPS | Random 4K write I/O operations per second |
|
||||
| Read Bandwidth | Sequential read throughput (MB/s) |
|
||||
| Write Bandwidth | Sequential write throughput (MB/s) |
|
||||
| Read Latency | Average read latency (µs) |
|
||||
| Write Latency | Average write latency (µs) |
|
||||
|
||||
## Stopping a Benchmark
|
||||
|
||||
Click **Stop** to cancel the running benchmark. The plugin will delete the Job and PVC.
|
||||
|
||||
If the page is closed or navigated away from during a benchmark, the Job and PVC will remain in the cluster with the label:
|
||||
|
||||
```
|
||||
app.kubernetes.io/managed-by=headlamp-tns-csi-plugin
|
||||
```
|
||||
|
||||
Clean them up manually:
|
||||
|
||||
```bash
|
||||
kubectl delete jobs,pvc -n <namespace> \
|
||||
-l app.kubernetes.io/managed-by=headlamp-tns-csi-plugin
|
||||
```
|
||||
|
||||
## Resource Cleanup
|
||||
|
||||
The plugin automatically deletes the benchmark Job and PVC when:
|
||||
- The benchmark completes successfully
|
||||
- You click Stop
|
||||
- The page component unmounts
|
||||
|
||||
## Protocol Notes
|
||||
|
||||
Different protocols have different performance characteristics:
|
||||
|
||||
| Protocol | Typical Use Case | Access Modes |
|
||||
| -------- | ---------------- | ------------ |
|
||||
| NFS | Shared storage, RWX workloads | RWO, RWX, RWOP |
|
||||
| NVMe-oF | High-performance block storage | RWO, RWOP |
|
||||
| iSCSI | Block storage | RWO, RWOP |
|
||||
|
||||
For NVMe-oF benchmarks, ensure nodes have the `nvme-tcp` kernel module loaded and the controller has a static IP.
|
||||
@@ -0,0 +1,121 @@
|
||||
# RBAC Permissions
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin requires different permissions depending on which features you use. Start with the read-only set and add the benchmark write permissions only if needed.
|
||||
|
||||
## Read-Only Permissions (All Pages Except Benchmark)
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: headlamp-tns-csi-reader
|
||||
rules:
|
||||
# StorageClasses and CSIDriver
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses", "csidrivers"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# PersistentVolumes (cluster-scoped)
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# PersistentVolumeClaims (all namespaces)
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# tns-csi driver pods and their logs/proxy (for metrics)
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods/log", "pods/proxy"]
|
||||
verbs: ["get"]
|
||||
|
||||
# VolumeSnapshots (optional — gracefully degraded if absent)
|
||||
- apiGroups: ["snapshot.storage.k8s.io"]
|
||||
resources: ["volumesnapshots", "volumesnapshotclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: headlamp-tns-csi
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp # adjust to your Headlamp service account name
|
||||
namespace: kube-system # adjust to your Headlamp namespace
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: headlamp-tns-csi-reader
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
## Additional Permissions for Benchmark Page
|
||||
|
||||
The Benchmark page creates and deletes a Job and PVC. These rules can be added to the ClusterRole above, or bound as a separate namespaced Role scoped to a dedicated benchmark namespace.
|
||||
|
||||
```yaml
|
||||
# Benchmark: create/delete kbench Job
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
|
||||
# Benchmark: create/delete kbench PVC
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
```
|
||||
|
||||
## Scoping Benchmark Permissions to a Namespace
|
||||
|
||||
For tighter security, restrict benchmark write permissions to a dedicated namespace using a Role + RoleBinding instead of ClusterRole:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: headlamp-tns-csi-benchmark
|
||||
namespace: storage-benchmarks # dedicated benchmark namespace
|
||||
rules:
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: headlamp-tns-csi-benchmark
|
||||
namespace: storage-benchmarks
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: headlamp
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: headlamp-tns-csi-benchmark
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
With this configuration, benchmark jobs can only be created in the `storage-benchmarks` namespace.
|
||||
|
||||
## Permission Summary by Feature
|
||||
|
||||
| Feature | Permissions Required |
|
||||
| ------- | -------------------- |
|
||||
| Overview | `storageclasses list`, `persistentvolumes list`, `persistentvolumeclaims list`, `pods list` (kube-system), `csidrivers get` |
|
||||
| Storage Classes | `storageclasses list` |
|
||||
| Volumes | `persistentvolumes list` |
|
||||
| Snapshots | `volumesnapshots list`, `volumesnapshotclasses list` |
|
||||
| Metrics | `pods/proxy get` (kube-system controller pod) |
|
||||
| Benchmark | `jobs create/delete`, `persistentvolumeclaims create/delete` |
|
||||
| PVC Detail Injection | `persistentvolumeclaims get`, `persistentvolumes get` |
|
||||
Binary file not shown.
Generated
+18188
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "headlamp-tns-csi-plugin",
|
||||
"version": "0.1.0",
|
||||
"name": "tns-csi",
|
||||
"version": "0.2.3",
|
||||
"description": "Headlamp plugin for TNS-CSI driver visibility and benchmarking",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -20,6 +20,12 @@ vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
ConfigStore: class {
|
||||
get() { return {}; }
|
||||
set() {}
|
||||
update() {}
|
||||
useConfig() { return () => ({}); }
|
||||
},
|
||||
}));
|
||||
|
||||
import { TnsCsiDataProvider, useTnsCsiContext } from './TnsCsiDataContext';
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
VolumeSnapshot,
|
||||
VolumeSnapshotClass,
|
||||
} from './k8s';
|
||||
import { fetchTruenasPoolStats, getTnsCsiConfig, PoolStats } from './truenas';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context shape
|
||||
@@ -46,6 +47,10 @@ export interface TnsCsiContextValue {
|
||||
volumeSnapshotClasses: VolumeSnapshotClass[];
|
||||
snapshotCrdAvailable: boolean;
|
||||
|
||||
// TrueNAS pool capacity (only populated when API key is configured)
|
||||
poolStats: PoolStats[];
|
||||
poolStatsError: string | null;
|
||||
|
||||
// Loading / error state
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
@@ -88,6 +93,8 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
const [asyncLoading, setAsyncLoading] = useState(true);
|
||||
const [asyncError, setAsyncError] = useState<string | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const [poolStats, setPoolStats] = useState<PoolStats[]>([]);
|
||||
const [poolStatsError, setPoolStatsError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setRefreshKey(k => k + 1);
|
||||
@@ -161,6 +168,31 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
setVolumeSnapshots([]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setAsyncError(err instanceof Error ? err.message : String(err));
|
||||
@@ -178,19 +210,34 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
// Derived / filtered values — memoized to avoid recomputation on every render
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Headlamp useList() returns KubeObject class instances that store raw Kubernetes
|
||||
// JSON under `.jsonData`. Direct property access only works for fields that have
|
||||
// explicit getter definitions in the class (e.g. provisioner, reclaimPolicy).
|
||||
// Fields like `parameters`, `spec`, `status` must be read from `.jsonData`.
|
||||
// We extract jsonData here so our plain-object type helpers work correctly.
|
||||
const extractJsonData = (items: unknown[]): unknown[] =>
|
||||
items.map(item =>
|
||||
item && typeof item === 'object' && 'jsonData' in item
|
||||
? (item as { jsonData: unknown }).jsonData
|
||||
: item
|
||||
);
|
||||
|
||||
const storageClasses = useMemo(() => {
|
||||
if (!allStorageClasses) return [];
|
||||
return filterTnsCsiStorageClasses(allStorageClasses as unknown[]);
|
||||
return filterTnsCsiStorageClasses(extractJsonData(allStorageClasses as unknown[]));
|
||||
}, [allStorageClasses]);
|
||||
|
||||
const persistentVolumes = useMemo(() => {
|
||||
if (!allPvs) return [];
|
||||
return filterTnsCsiPersistentVolumes(allPvs as unknown[]);
|
||||
return filterTnsCsiPersistentVolumes(extractJsonData(allPvs as unknown[]));
|
||||
}, [allPvs]);
|
||||
|
||||
const persistentVolumeClaims = useMemo(() => {
|
||||
if (!allPvcs || persistentVolumes.length === 0) return [];
|
||||
return filterTnsCsiPVCs(allPvcs as TnsCsiPersistentVolumeClaim[], persistentVolumes);
|
||||
return filterTnsCsiPVCs(
|
||||
extractJsonData(allPvcs as unknown[]) as TnsCsiPersistentVolumeClaim[],
|
||||
persistentVolumes
|
||||
);
|
||||
}, [allPvcs, persistentVolumes]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -224,6 +271,8 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
volumeSnapshots,
|
||||
volumeSnapshotClasses,
|
||||
snapshotCrdAvailable,
|
||||
poolStats,
|
||||
poolStatsError,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
@@ -239,6 +288,8 @@ export function TnsCsiDataProvider({ children }: { children: React.ReactNode })
|
||||
volumeSnapshots,
|
||||
volumeSnapshotClasses,
|
||||
snapshotCrdAvailable,
|
||||
poolStats,
|
||||
poolStatsError,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* TrueNAS API client for the headlamp-tns-csi-plugin.
|
||||
*
|
||||
* Uses the TrueNAS WebSocket JSON-RPC 2.0 API to fetch pool-level capacity
|
||||
* information (pool.query). Requires a TrueNAS API key configured by the user
|
||||
* in plugin settings.
|
||||
*
|
||||
* The WebSocket connects directly from the browser to the TrueNAS server.
|
||||
* The server address comes from the StorageClass parameters (already in context).
|
||||
*
|
||||
* All operations are read-only (pool.query only).
|
||||
*/
|
||||
|
||||
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config store — persists across sessions via Headlamp Redux store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TnsCsiConfig {
|
||||
truenasApiKey: string;
|
||||
/** Override server address (defaults to StorageClass parameter 'server') */
|
||||
truenasServerOverride: string;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: TnsCsiConfig = {
|
||||
truenasApiKey: '',
|
||||
truenasServerOverride: '',
|
||||
};
|
||||
|
||||
const configStore = new ConfigStore<TnsCsiConfig>('headlamp-tns-csi-plugin');
|
||||
|
||||
export function getTnsCsiConfig(): TnsCsiConfig {
|
||||
return { ...DEFAULT_CONFIG, ...configStore.get() };
|
||||
}
|
||||
|
||||
export function setTnsCsiConfig(partial: Partial<TnsCsiConfig>): void {
|
||||
configStore.update(partial);
|
||||
}
|
||||
|
||||
export function useTnsCsiConfig(): () => TnsCsiConfig {
|
||||
return configStore.useConfig();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PoolStats {
|
||||
name: string;
|
||||
/** Total pool capacity in bytes */
|
||||
size: number;
|
||||
/** Allocated (used) bytes */
|
||||
allocated: number;
|
||||
/** Free bytes */
|
||||
free: number;
|
||||
/** Pool health status string */
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TrueNAS WebSocket JSON-RPC client
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Opens a WebSocket to TrueNAS, authenticates with the API key, calls
|
||||
* pool.query, collects results, and closes the connection.
|
||||
*
|
||||
* @param server - TrueNAS host/IP (no protocol prefix)
|
||||
* @param apiKey - TrueNAS API key
|
||||
* @returns Array of pool stats
|
||||
*/
|
||||
export function fetchTruenasPoolStats(
|
||||
server: string,
|
||||
apiKey: string
|
||||
): Promise<PoolStats[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// TrueNAS WebSocket endpoint — supports both SCALE and CORE
|
||||
const url = `wss://${server}/api/current`;
|
||||
let ws: WebSocket;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
ws?.close();
|
||||
reject(new Error('TrueNAS connection timed out (10s)'));
|
||||
}, 10_000);
|
||||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
} catch (err) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Failed to open WebSocket to ${server}: ${String(err)}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let msgId = 1;
|
||||
// State machine: connect → authenticate → query → done
|
||||
type Phase = 'connecting' | 'authenticating' | 'querying' | 'done';
|
||||
let phase: Phase = 'connecting';
|
||||
|
||||
ws.onopen = () => {
|
||||
phase = 'authenticating';
|
||||
ws.send(JSON.stringify({
|
||||
id: msgId++,
|
||||
msg: 'method',
|
||||
method: 'auth.login_with_api_key',
|
||||
params: [apiKey],
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
let msg: Record<string, unknown>;
|
||||
try {
|
||||
msg = JSON.parse(event.data as string) as Record<string, unknown>;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === 'authenticating') {
|
||||
const result = msg['result'];
|
||||
if (result !== true) {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
reject(new Error('TrueNAS authentication failed — check your API key'));
|
||||
return;
|
||||
}
|
||||
phase = 'querying';
|
||||
ws.send(JSON.stringify({
|
||||
id: msgId++,
|
||||
msg: 'method',
|
||||
method: 'pool.query',
|
||||
params: [],
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === 'querying') {
|
||||
const result = msg['result'];
|
||||
if (!Array.isArray(result)) {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
reject(new Error('pool.query returned unexpected result'));
|
||||
return;
|
||||
}
|
||||
phase = 'done';
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
|
||||
const pools: PoolStats[] = result.map((pool: unknown) => {
|
||||
const p = pool as Record<string, unknown>;
|
||||
return {
|
||||
name: String(p['name'] ?? ''),
|
||||
size: Number(p['size'] ?? 0),
|
||||
allocated: Number(p['allocated'] ?? 0),
|
||||
free: Number(p['free'] ?? 0),
|
||||
status: String(p['status'] ?? 'UNKNOWN'),
|
||||
};
|
||||
});
|
||||
resolve(pools);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
if (phase !== 'done') {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`WebSocket error connecting to ${server} — check the server address and that TrueNAS is reachable`));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event: CloseEvent) => {
|
||||
if (phase !== 'done') {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`WebSocket closed unexpectedly (code ${event.code}) while ${phase}`));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* AppBarDriverBadge — registerAppBarAction driver health badge.
|
||||
*
|
||||
* Displays "tns-csi: N/N" in the Headlamp top nav bar showing
|
||||
* ready controller + node pod counts. Color-coded:
|
||||
* green = all pods ready
|
||||
* orange = some pods degraded
|
||||
* red = no pods ready or driver missing
|
||||
*
|
||||
* Returns null if the driver is not installed (no CSIDriver object) --
|
||||
* no clutter in clusters where tns-csi is absent.
|
||||
*
|
||||
* Wrapped in TnsCsiDataProvider at registration time (index.tsx).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isPodReady, TnsCsiPod } from '../api/k8s';
|
||||
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
|
||||
|
||||
function countReady(pods: TnsCsiPod[]): number {
|
||||
return pods.filter(isPodReady).length;
|
||||
}
|
||||
|
||||
function getBadgeColor(ready: number, total: number): string {
|
||||
if (total === 0) return '#9e9e9e';
|
||||
if (ready === total) return '#4caf50';
|
||||
if (ready > 0) return '#ff9800';
|
||||
return '#f44336';
|
||||
}
|
||||
|
||||
export default function AppBarDriverBadge() {
|
||||
const { driverInstalled, controllerPods, nodePods, loading } = useTnsCsiContext();
|
||||
const history = useHistory();
|
||||
|
||||
if (loading || !driverInstalled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const controllerReady = countReady(controllerPods);
|
||||
const controllerTotal = controllerPods.length;
|
||||
const nodeReady = countReady(nodePods);
|
||||
const nodeTotal = nodePods.length;
|
||||
|
||||
const totalReady = controllerReady + nodeReady;
|
||||
const totalPods = controllerTotal + nodeTotal;
|
||||
|
||||
const color = getBadgeColor(totalReady, totalPods);
|
||||
|
||||
const handleClick = () => {
|
||||
history.push('/tns-csi');
|
||||
};
|
||||
|
||||
const labelText = `tns-csi: ${controllerReady}/${controllerTotal}c ${nodeReady}/${nodeTotal}n`;
|
||||
const ariaLabel = `TNS-CSI driver: ${controllerReady} of ${controllerTotal} controller pods ready, ${nodeReady} of ${nodeTotal} node pods ready`;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginRight: '8px',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '16px',
|
||||
border: 'none',
|
||||
backgroundColor: color,
|
||||
color: 'white',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
aria-label={ariaLabel}
|
||||
title={ariaLabel}
|
||||
>
|
||||
<span>tns-csi: {controllerReady}/{controllerTotal}c {nodeReady}/{nodeTotal}n</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -58,6 +58,8 @@ export default function OverviewPage() {
|
||||
persistentVolumeClaims,
|
||||
controllerPods,
|
||||
nodePods,
|
||||
poolStats,
|
||||
poolStatsError,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
@@ -109,6 +111,27 @@ export default function OverviewPage() {
|
||||
const chartData = protocolChartData(storageClasses);
|
||||
const totalScs = storageClasses.length;
|
||||
|
||||
// Capacity by pool: join volumeCapacityBytes samples (volume_id, protocol)
|
||||
// with PV volumeHandle → pool name from volumeAttributes.
|
||||
const capacityByPool: Map<string, number> = React.useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
if (!metrics) return map;
|
||||
// Build lookup: volumeHandle → pool name
|
||||
const handleToPool = new Map<string, string>();
|
||||
for (const pv of persistentVolumes) {
|
||||
const handle = pv.spec.csi?.volumeHandle;
|
||||
const pool = pv.spec.csi?.volumeAttributes?.['pool'];
|
||||
if (handle && pool) handleToPool.set(handle, pool);
|
||||
}
|
||||
for (const sample of metrics.volumeCapacityBytes) {
|
||||
const volumeId = sample.labels['volume_id'];
|
||||
if (!volumeId) continue;
|
||||
const pool = handleToPool.get(volumeId) ?? 'unknown';
|
||||
map.set(pool, (map.get(pool) ?? 0) + sample.value);
|
||||
}
|
||||
return map;
|
||||
}, [metrics, persistentVolumes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
@@ -234,6 +257,66 @@ export default function OverviewPage() {
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
{/* Pool capacity — real data from TrueNAS API when configured */}
|
||||
{poolStats.length > 0 && (
|
||||
<SectionBox title="Pool Capacity">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Pool', getter: (p) => p.name },
|
||||
{
|
||||
label: 'Status',
|
||||
getter: (p) => (
|
||||
<StatusLabel status={p.status === 'ONLINE' ? 'success' : 'warning'}>
|
||||
{p.status}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{ label: 'Total', getter: (p) => formatBytes(p.size) },
|
||||
{ label: 'Used', getter: (p) => formatBytes(p.allocated) },
|
||||
{ label: 'Free', getter: (p) => formatBytes(p.free) },
|
||||
{
|
||||
label: 'Used %',
|
||||
getter: (p) => p.size > 0
|
||||
? `${Math.round((p.allocated / p.size) * 100)}%`
|
||||
: '—',
|
||||
},
|
||||
]}
|
||||
data={poolStats}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{poolStatsError && (
|
||||
<SectionBox title="Pool Capacity Unavailable">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Error',
|
||||
value: <StatusLabel status="warning">{poolStatsError}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: 'Check your TrueNAS API key and server address in plugin settings.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{/* Provisioned capacity by pool (from Prometheus metrics — shown when TrueNAS API not configured) */}
|
||||
{poolStats.length === 0 && !poolStatsError && capacityByPool.size > 0 && (
|
||||
<SectionBox title="Provisioned Capacity by Pool">
|
||||
<NameValueTable
|
||||
rows={[...capacityByPool.entries()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([pool, bytes]) => ({
|
||||
name: pool,
|
||||
value: formatBytes(bytes),
|
||||
}))}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{/* Non-bound PVCs warning */}
|
||||
{nonBoundPvcs.length > 0 && (
|
||||
<SectionBox title="Attention: Non-Bound PVCs">
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* PVDetailSection — injected into Headlamp's PersistentVolume detail view.
|
||||
*
|
||||
* Shown only when the PV uses tns.csi.io as the CSI driver.
|
||||
* Uses registerDetailsViewSection in index.tsx.
|
||||
*/
|
||||
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { formatProtocol, TNS_CSI_PROVISIONER } from '../api/k8s';
|
||||
|
||||
interface PVDetailSectionProps {
|
||||
resource: {
|
||||
kind?: string;
|
||||
metadata?: { name?: string; namespace?: string };
|
||||
spec?: {
|
||||
csi?: {
|
||||
driver?: string;
|
||||
volumeHandle?: string;
|
||||
volumeAttributes?: Record<string, string>;
|
||||
};
|
||||
storageClassName?: string;
|
||||
capacity?: { storage?: string };
|
||||
persistentVolumeReclaimPolicy?: string;
|
||||
};
|
||||
// KubeObject instance — raw JSON lives under jsonData
|
||||
jsonData?: {
|
||||
spec?: {
|
||||
csi?: {
|
||||
driver?: string;
|
||||
volumeHandle?: string;
|
||||
volumeAttributes?: Record<string, string>;
|
||||
};
|
||||
storageClassName?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function PVDetailSection({ resource }: PVDetailSectionProps) {
|
||||
// Extract from jsonData (KubeObject instance) or fall back to direct properties
|
||||
const spec = resource?.jsonData?.spec ?? resource?.spec;
|
||||
const csi = spec?.csi;
|
||||
|
||||
if (!csi || csi.driver !== TNS_CSI_PROVISIONER) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attrs = csi.volumeAttributes ?? {};
|
||||
const protocol = formatProtocol(attrs['protocol']);
|
||||
const otherAttrs = Object.entries(attrs).filter(
|
||||
([k]) => !['protocol', 'server', 'pool'].includes(k)
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionBox title="TNS-CSI Storage Details">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{ name: 'Driver', value: TNS_CSI_PROVISIONER },
|
||||
{ name: 'Protocol', value: protocol },
|
||||
{ name: 'Server', value: attrs['server'] ?? '—' },
|
||||
{ name: 'Pool', value: attrs['pool'] ?? '—' },
|
||||
{ name: 'Volume Handle', value: csi.volumeHandle ?? '—' },
|
||||
{ name: 'Storage Class', value: spec?.storageClassName ?? '—' },
|
||||
...otherAttrs.map(([k, v]) => ({ name: k, value: v ?? '—' })),
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* TnsCsiSettings — plugin settings page.
|
||||
*
|
||||
* Lets users configure the TrueNAS API key and (optionally) a server address
|
||||
* override. When configured, the plugin fetches real pool capacity data via
|
||||
* the TrueNAS WebSocket JSON-RPC API (pool.query) and displays it on the
|
||||
* Overview page.
|
||||
*
|
||||
* Settings are persisted via Headlamp's ConfigStore (Redux-backed).
|
||||
*/
|
||||
|
||||
import {
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React, { useState } from 'react';
|
||||
import { fetchTruenasPoolStats, getTnsCsiConfig, setTnsCsiConfig } from '../api/truenas';
|
||||
|
||||
interface PluginSettingsProps {
|
||||
data?: Record<string, string | number | boolean>;
|
||||
onDataChange?: (data: Record<string, string | number | boolean>) => void;
|
||||
}
|
||||
|
||||
const INPUT_STYLE: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '4px 8px',
|
||||
border: '1px solid var(--mui-palette-divider, #e0e0e0)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
|
||||
color: 'var(--mui-palette-text-primary, #000)',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
const HINT_STYLE: React.CSSProperties = {
|
||||
fontSize: '12px',
|
||||
color: 'var(--mui-palette-text-secondary, #666)',
|
||||
marginTop: '4px',
|
||||
};
|
||||
|
||||
export default function TnsCsiSettings({ data, onDataChange }: PluginSettingsProps) {
|
||||
const saved = getTnsCsiConfig();
|
||||
|
||||
const [apiKey, setApiKey] = useState<string>(
|
||||
(data?.truenasApiKey as string) ?? saved.truenasApiKey ?? ''
|
||||
);
|
||||
const [serverOverride, setServerOverride] = useState<string>(
|
||||
(data?.truenasServerOverride as string) ?? saved.truenasServerOverride ?? ''
|
||||
);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
|
||||
function handleApiKeyChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const val = e.target.value;
|
||||
setApiKey(val);
|
||||
setTnsCsiConfig({ truenasApiKey: val });
|
||||
onDataChange?.({ ...data, truenasApiKey: val });
|
||||
}
|
||||
|
||||
function handleServerOverrideChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const val = e.target.value;
|
||||
setServerOverride(val);
|
||||
setTnsCsiConfig({ truenasServerOverride: val });
|
||||
onDataChange?.({ ...data, truenasServerOverride: val });
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
const server = serverOverride.trim() || '(from StorageClass)';
|
||||
if (!serverOverride.trim()) {
|
||||
setTesting(false);
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: 'Enter a Server Address to test the connection.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!apiKey.trim()) {
|
||||
setTesting(false);
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: 'Enter an API key to test the connection.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const pools = await fetchTruenasPoolStats(serverOverride.trim(), apiKey.trim());
|
||||
const names = pools.map(p => p.name).join(', ');
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `Connected to ${server}. Found ${pools.length} pool(s): ${names || '(none)'}`,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
message: String(err instanceof Error ? err.message : err),
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionBox title="TrueNAS API (Optional)">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'API Key',
|
||||
value: (
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={handleApiKeyChange}
|
||||
placeholder="Paste your TrueNAS API key here"
|
||||
style={INPUT_STYLE}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<div style={HINT_STYLE}>
|
||||
Generate in TrueNAS UI → Credentials → API Keys.
|
||||
Required for real pool capacity data on the Overview page.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Server Address',
|
||||
value: (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={serverOverride}
|
||||
onChange={handleServerOverrideChange}
|
||||
placeholder="e.g. 192.168.1.100 or truenas.local"
|
||||
style={INPUT_STYLE}
|
||||
/>
|
||||
<div style={HINT_STYLE}>
|
||||
TrueNAS host/IP. If blank, the plugin uses the{' '}
|
||||
<code>server</code> parameter from your tns-csi StorageClass.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Connection Test',
|
||||
value: (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => void testConnection()}
|
||||
disabled={testing}
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
backgroundColor: testing
|
||||
? 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||
: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
color: testing
|
||||
? 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||
: 'var(--mui-palette-primary-contrastText, #fff)',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: testing ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{testing ? 'Testing…' : 'Test Connection'}
|
||||
</button>
|
||||
{testResult && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<StatusLabel status={testResult.success ? 'success' : 'error'}>
|
||||
{testResult.message}
|
||||
</StatusLabel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* StorageClassBenchmarkButton — registerDetailsViewHeaderAction for StorageClass pages.
|
||||
*
|
||||
* Adds a "Benchmark" button to the detail page header of tns-csi StorageClasses.
|
||||
* Navigates to /tns-csi/benchmark so the user can run a FIO benchmark
|
||||
* against that storage class.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { TNS_CSI_PROVISIONER } from '../../api/k8s';
|
||||
|
||||
interface StorageClassBenchmarkButtonProps {
|
||||
resource: {
|
||||
provisioner?: string;
|
||||
metadata?: { name?: string };
|
||||
// KubeObject instance — provisioner may be a direct getter or under jsonData
|
||||
jsonData?: {
|
||||
provisioner?: string;
|
||||
metadata?: { name?: string };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function StorageClassBenchmarkButton({ resource }: StorageClassBenchmarkButtonProps) {
|
||||
const history = useHistory();
|
||||
|
||||
// provisioner is one of the fields Headlamp's StorageClass class exposes as a getter,
|
||||
// so it's accessible directly. jsonData fallback for safety.
|
||||
const provisioner =
|
||||
resource?.provisioner ??
|
||||
resource?.jsonData?.provisioner;
|
||||
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scName =
|
||||
resource?.metadata?.name ??
|
||||
resource?.jsonData?.metadata?.name ??
|
||||
'';
|
||||
|
||||
const handleClick = () => {
|
||||
// Navigate to benchmark page; user selects the SC in the benchmark form.
|
||||
// Pass the SC name via hash so BenchmarkPage can pre-select it if desired.
|
||||
history.push(`/tns-csi/benchmark#${scName}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '6px 16px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid currentColor',
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
opacity: 0.85,
|
||||
}}
|
||||
aria-label={`Run benchmark on ${scName}`}
|
||||
title={`Run FIO benchmark on storage class ${scName}`}
|
||||
>
|
||||
<span>⚡</span>
|
||||
<span>Benchmark</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* StorageClassColumns — registerResourceTableColumnsProcessor for StorageClass and PV tables.
|
||||
*
|
||||
* Adds Protocol/Pool/Server columns to the native /storage-classes table and
|
||||
* 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
|
||||
* via .jsonData — only fields with explicit getters (provisioner, reclaimPolicy, etc.)
|
||||
* are accessible as direct properties.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { formatProtocol, TNS_CSI_PROVISIONER } from '../../api/k8s';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: extract a field from either a KubeObject instance or a plain object
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getField(item: unknown, ...path: string[]): unknown {
|
||||
if (!item || typeof item !== 'object') return undefined;
|
||||
const obj = item as Record<string, unknown>;
|
||||
|
||||
// KubeObject instance — raw K8s JSON is under .jsonData
|
||||
const raw: Record<string, unknown> =
|
||||
'jsonData' in obj && obj['jsonData'] && typeof obj['jsonData'] === 'object'
|
||||
? (obj['jsonData'] as Record<string, unknown>)
|
||||
: obj;
|
||||
|
||||
let cur: unknown = raw;
|
||||
for (const key of path) {
|
||||
if (!cur || typeof cur !== 'object') return undefined;
|
||||
cur = (cur as Record<string, unknown>)[key];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// StorageClass column definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns extra columns for the native StorageClass table.
|
||||
* For non-tns-csi rows, cells show "—" (never undefined/null visible).
|
||||
*/
|
||||
export function buildStorageClassColumns() {
|
||||
return [
|
||||
{
|
||||
label: 'Protocol',
|
||||
getValue: (sc: unknown): string | null => {
|
||||
const provisioner =
|
||||
getField(sc, 'provisioner') ??
|
||||
(sc as Record<string, unknown>)?.['provisioner'];
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) return null;
|
||||
const p = getField(sc, 'parameters', 'protocol');
|
||||
return typeof p === 'string' ? formatProtocol(p) : null;
|
||||
},
|
||||
render: (sc: unknown) => {
|
||||
const provisioner =
|
||||
getField(sc, 'provisioner') ??
|
||||
(sc as Record<string, unknown>)?.['provisioner'];
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
||||
const protocol = getField(sc, 'parameters', 'protocol') as string | undefined;
|
||||
return <span>{formatProtocol(protocol)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Pool',
|
||||
getValue: (sc: unknown): string | null => {
|
||||
const provisioner =
|
||||
getField(sc, 'provisioner') ??
|
||||
(sc as Record<string, unknown>)?.['provisioner'];
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) return null;
|
||||
const p = getField(sc, 'parameters', 'pool');
|
||||
return typeof p === 'string' ? p : null;
|
||||
},
|
||||
render: (sc: unknown) => {
|
||||
const provisioner =
|
||||
getField(sc, 'provisioner') ??
|
||||
(sc as Record<string, unknown>)?.['provisioner'];
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
||||
const pool = getField(sc, 'parameters', 'pool') as string | undefined;
|
||||
return <span>{pool ?? '—'}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Server',
|
||||
getValue: (sc: unknown): string | null => {
|
||||
const provisioner =
|
||||
getField(sc, 'provisioner') ??
|
||||
(sc as Record<string, unknown>)?.['provisioner'];
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) return null;
|
||||
const p = getField(sc, 'parameters', 'server');
|
||||
return typeof p === 'string' ? p : null;
|
||||
},
|
||||
render: (sc: unknown) => {
|
||||
const provisioner =
|
||||
getField(sc, 'provisioner') ??
|
||||
(sc as Record<string, unknown>)?.['provisioner'];
|
||||
if (provisioner !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
||||
const server = getField(sc, 'parameters', 'server') as string | undefined;
|
||||
return <span>{server ?? '—'}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PersistentVolume column definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns extra columns for the native PersistentVolume table.
|
||||
* For non-tns-csi PVs, cells show "—".
|
||||
*/
|
||||
export function buildPVColumns() {
|
||||
return [
|
||||
{
|
||||
label: 'Protocol',
|
||||
getValue: (pv: unknown): string | null => {
|
||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||
if (driver !== TNS_CSI_PROVISIONER) return null;
|
||||
const p = getField(pv, 'spec', 'csi', 'volumeAttributes', 'protocol');
|
||||
return typeof p === 'string' ? formatProtocol(p) : null;
|
||||
},
|
||||
render: (pv: unknown) => {
|
||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||
if (driver !== TNS_CSI_PROVISIONER) return <span>—</span>;
|
||||
const protocol = getField(pv, 'spec', 'csi', 'volumeAttributes', 'protocol') as string | undefined;
|
||||
return <span>{formatProtocol(protocol)}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Pool',
|
||||
getValue: (pv: unknown): string | null => {
|
||||
const driver = getField(pv, 'spec', 'csi', 'driver') as string | undefined;
|
||||
if (driver !== TNS_CSI_PROVISIONER) return 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 dataset = getField(pv, 'spec', 'csi', 'volumeAttributes', 'datasetName') as string | undefined;
|
||||
const pool = dataset?.split('/')[0];
|
||||
return <span>{pool ?? '—'}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
+88
-36
@@ -1,34 +1,42 @@
|
||||
/**
|
||||
* headlamp-tns-csi-plugin — entry point.
|
||||
*
|
||||
* Registers sidebar entries, routes, detail view section, and plugin settings
|
||||
* for the tns-csi CSI driver Headlamp plugin.
|
||||
* Registers sidebar entries, routes, detail view sections, table column
|
||||
* processors, header actions, and app bar action for the tns-csi CSI driver.
|
||||
*/
|
||||
|
||||
import {
|
||||
registerDetailsViewHeaderAction,
|
||||
registerDetailsViewSection,
|
||||
registerPluginSettings,
|
||||
registerResourceTableColumnsProcessor,
|
||||
registerRoute,
|
||||
registerSidebarEntry,
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
import { TnsCsiDataProvider } from './api/TnsCsiDataContext';
|
||||
import TnsCsiSettings from './components/TnsCsiSettings';
|
||||
import BenchmarkPage from './components/BenchmarkPage';
|
||||
import DriverPodDetailSection from './components/DriverPodDetailSection';
|
||||
import { buildPVColumns, buildStorageClassColumns } from './components/integrations/StorageClassColumns';
|
||||
import StorageClassBenchmarkButton from './components/integrations/StorageClassBenchmarkButton';
|
||||
import MetricsPage from './components/MetricsPage';
|
||||
import OverviewPage from './components/OverviewPage';
|
||||
import PVCDetailSection from './components/PVCDetailSection';
|
||||
import PVDetailSection from './components/PVDetailSection';
|
||||
import SnapshotsPage from './components/SnapshotsPage';
|
||||
import StorageClassesPage from './components/StorageClassesPage';
|
||||
import VolumesPage from './components/VolumesPage';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar entries
|
||||
// Sidebar entries (trimmed from 6 to 4 — Storage Classes and Volumes now
|
||||
// surface via native Headlamp tables with injected columns/sections)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: null,
|
||||
name: 'tns-csi',
|
||||
label: 'TNS CSI',
|
||||
label: 'TrueNAS (tns-csi)',
|
||||
url: '/tns-csi',
|
||||
icon: 'mdi:database-cog',
|
||||
});
|
||||
@@ -41,22 +49,6 @@ registerSidebarEntry({
|
||||
icon: 'mdi:view-dashboard',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'tns-csi',
|
||||
name: 'tns-csi-storage-classes',
|
||||
label: 'Storage Classes',
|
||||
url: '/tns-csi/storage-classes',
|
||||
icon: 'mdi:database',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'tns-csi',
|
||||
name: 'tns-csi-volumes',
|
||||
label: 'Volumes',
|
||||
url: '/tns-csi/volumes',
|
||||
icon: 'mdi:harddisk',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'tns-csi',
|
||||
name: 'tns-csi-snapshots',
|
||||
@@ -82,7 +74,7 @@ registerSidebarEntry({
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Routes
|
||||
// Routes (keep all routes so direct links still work)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerRoute({
|
||||
@@ -97,9 +89,11 @@ registerRoute({
|
||||
),
|
||||
});
|
||||
|
||||
// Routes for storage-classes and volumes are kept for direct URL access
|
||||
// but are no longer in the sidebar — native Headlamp tables have tns-csi columns.
|
||||
registerRoute({
|
||||
path: '/tns-csi/storage-classes',
|
||||
sidebar: 'tns-csi-storage-classes',
|
||||
sidebar: 'tns-csi-overview',
|
||||
name: 'tns-csi-storage-classes',
|
||||
exact: true,
|
||||
component: () => (
|
||||
@@ -111,7 +105,7 @@ registerRoute({
|
||||
|
||||
registerRoute({
|
||||
path: '/tns-csi/volumes',
|
||||
sidebar: 'tns-csi-volumes',
|
||||
sidebar: 'tns-csi-overview',
|
||||
name: 'tns-csi-volumes',
|
||||
exact: true,
|
||||
component: () => (
|
||||
@@ -158,7 +152,7 @@ registerRoute({
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PVC detail view injection
|
||||
// Detail view section — PVC pages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
@@ -171,19 +165,77 @@ registerDetailsViewSection(({ resource }) => {
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detail view section — PV pages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
if (resource?.kind !== 'PersistentVolume') return null;
|
||||
return <PVDetailSection resource={resource} />;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detail view section — Pod pages (tns-csi driver pods only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
if (resource?.kind !== 'Pod') return null;
|
||||
return <DriverPodDetailSection resource={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 mergeColumns(columns, buildStorageClassColumns());
|
||||
}
|
||||
if (id === 'headlamp-persistentvolumes') {
|
||||
return mergeColumns(columns, buildPVColumns());
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header action — StorageClass detail page Benchmark shortcut
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerDetailsViewHeaderAction(({ resource }) => {
|
||||
if (resource?.kind !== 'StorageClass') return null;
|
||||
return <StorageClassBenchmarkButton resource={resource} />;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function TnsCsiSettings() {
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<p style={{ color: 'var(--mui-palette-text-secondary)' }}>
|
||||
TNS-CSI plugin settings. Configure defaults below.
|
||||
</p>
|
||||
{/* Future: default namespace, metrics refresh interval, auto-cleanup setting */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
registerPluginSettings('headlamp-tns-csi-plugin', TnsCsiSettings, true);
|
||||
registerPluginSettings('tns-csi', TnsCsiSettings, true);
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user