feat: native Headlamp integration, TrueNAS API, docs, and CI for v0.2.0
Native Headlamp integrations: - registerResourceTableColumnsProcessor: add Protocol/Pool/Server columns to native StorageClass table and Protocol/Volume Handle to PV table - registerDetailsViewSection: inject TNS-CSI section into PV detail pages - registerDetailsViewSection: inject driver role/status into tns-csi Pod pages - registerDetailsViewHeaderAction: Benchmark shortcut on StorageClass detail - registerAppBarAction: driver health badge (N/Nc M/Mn, color-coded) - Trim sidebar from 6 → 4 entries (Overview, Snapshots, Metrics, Benchmark) TrueNAS API integration: - src/api/truenas.ts: ConfigStore-backed settings, WebSocket JSON-RPC client for pool.query (auth.login_with_api_key + pool.query) - src/components/TnsCsiSettings.tsx: API key + server override settings UI with connection test button - TnsCsiDataContext: fetch real pool stats (size/allocated/free/status) - OverviewPage: three-tier pool capacity display (real data → error → metrics fallback) Documentation: - README, CHANGELOG, CONTRIBUTING, SECURITY - docs/: architecture, deployment (Helm), getting-started, user-guide, troubleshooting CI: - .github/workflows/ci.yaml: lint + type-check + test on PR/push - .github/workflows/release.yaml: workflow_dispatch versioned release Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -0,0 +1,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` |
|
||||
Reference in New Issue
Block a user