nas-admin
NAS PV/PVC admin panel for the LG N2R1 SMBv1 NAS. FastAPI + HTMX + Tailwind CDN app deployed as a Helm chart. Covers the app source, Kaniko build pattern, Helm chart structure, auth options (basic/oid
SKILL.mdskills/nas-admin/SKILL.md
GitHubNAS Admin Panel
Web admin panel for managing Kubernetes PersistentVolumes and PersistentVolumeClaims
backed by the cluster's SMB NAS (LG N2R1, 192.168.178.102, SMBv1 only).
Why it exists
The LG N2R1's built-in web UI shows disk usage but has no visibility into which K8s workloads own which volumes. Over time, PVs/PVCs accumulate (released, unused, or orphaned). This panel provides that visibility and a delete action.
Key paths
| Path | Purpose |
|---|---|
apps/nas-admin/main.py |
FastAPI app — edit here to change backend logic |
apps/nas-admin/templates/ |
Jinja2 + HTMX HTML — edit here to change UI |
apps/nas-admin/Dockerfile |
Docker build — identical to what Kaniko uses |
charts/nas-admin/values.yaml |
All knobs: auth, hostname, PVC, resources |
roles/install-nas-admin-image/ |
Kaniko in-cluster ARM64 build |
roles/install-nas-admin/ |
Helm install/upgrade via kubernetes.core.helm |
Makefile targets
make nas-admin-build # Kaniko build → registry.registry:5000/ai/nas-admin:latest (~2 min)
# Uses playbooks/build-nas-admin.yml (standalone, no cluster bootstrap)
make nas-admin-build-logs # Tail build logs while Kaniko runs (separate terminal)
make nas-admin # Helm install/upgrade (no rebuild)
make nas-admin-all # build + deploy (idempotent)
The nas-admin-build target uses a standalone playbook (playbooks/build-nas-admin.yml)
that runs only the install-nas-admin-image role with gather_facts: false.
This is faster than bootstrap.yml --tags nas-admin-build for quick iteration.
Iteration workflow
Fast local test (no cluster deploy):
docker build -t nas-admin:test ./infra/apps/nas-admin/
docker run -p 8080:8080 -v ~/.kube/config:/root/.kube/config:ro nas-admin:test
# open http://localhost:8080
Change HTML/Python → test locally → push to cluster:
# 1. edit apps/nas-admin/
# 2. make nas-admin-build (Kaniko re-reads files from role ConfigMap via lookup('file', ...))
# 3. make nas-admin (Helm upgrade, rolling restart)
Change only config (auth mode, password, hostname) → no rebuild needed:
# edit roles/install-nas-admin/defaults/main.yml (or secrets.yml for passwords)
make nas-admin # only helm upgrade
Features
- PV view (
/pvs): all SMB-backed PVs, status badges, NAS share path, bound claim, age. Orphaned PVs (Released/Available/Failed) highlighted amber. Delete button visible only on orphaned rows — removes K8s object, NAS data untouched (Retain policy). - PVC view (
/pvcs): all PVCs cross-namespace (or SMB-only filter). Shows which pod(s) mount each PVC. "Unused" PVCs (bound but no running pod) highlighted. Delete button with HTMXhx-confirm. - NAS browser (
/browse): HTMX-driven directory navigation. Reads from/mnt/nas(NAS PVC mounted ReadOnly). No direct SMBv1 connection from Python. - Health (
/health): returns{"status":"ok"}— used by liveness/readiness probes.
PV/PVC cleanup flow
When you delete a PVC from the UI with Retain policy:
- PVC is deleted → pod can no longer mount it
- PV status changes
Bound → Released— row turns amber - Click Delete on the PV row →
DELETE /api/pvs/{name}→ K8s PV object removed - NAS share data (
//192.168.178.102/service/...) is not touched — only the K8s object is gone
For dynamic PVs (StorageClass smb-nas with Delete reclaim policy), deleting the PVC also deletes the PV automatically — no manual PV cleanup needed.
Authentication
Configured via auth.mode in values.yaml or Ansible nas_admin_auth_mode.
Basic auth (default)
auth:
mode: basic
basic:
username: admin
password: "changeme" # set in roles/install-nas-admin/defaults/secrets.yml (gitignored)
The Helm chart creates a K8s Secret → pod reads AUTH_USERNAME / AUTH_PASSWORD env vars.
FastAPI uses HTTPBasic with constant-time comparison (secrets.compare_digest).
OIDC via GitHub (oauth2-proxy sidecar)
auth:
mode: oidc
oidc:
clientId: "..."
clientSecret: "..."
cookieSecret: "..." # openssl rand -base64 32
emailDomain: "*"
When auth.mode == "oidc":
- A
quay.io/oauth2-proxy/oauth2-proxy:v7.6.0sidecar is added to the Deployment - The sidecar listens on port 4180, proxies authenticated traffic to
localhost:8080 - The Service and HTTPRoute point to port 4180 (not 8080)
- A
SecretwithclientId,clientSecret,cookieSecretis created
Prerequisite: GitHub OAuth App with callback URL https://nas-admin.cluster.home/oauth2/callback.
Works on *.cluster.home as long as the browser resolves it via Pi-hole DNS on the local network.
Helm chart parameters
All configurable via charts/nas-admin/values.yaml or --set:
| Parameter | Default | Description |
|---|---|---|
image.repository |
registry.registry:5000/ai/nas-admin |
Image registry path |
image.tag |
latest |
Image tag |
hostname |
nas-admin.cluster.home |
HTTPRoute hostname |
nasPvc.enabled |
true |
Mount smb-nas-pvc for file browser |
nasPvc.claimName |
nas-admin-nas-pvc |
PVC to mount (dedicated PV in storage ns) |
nasPvc.mountPath |
/mnt/nas |
Mount path in pod |
auth.mode |
basic |
basic | oidc | none |
auth.basic.username |
admin |
Basic auth username |
auth.basic.password |
changeme |
Basic auth password |
replicaCount |
1 |
Number of replicas |
Kaniko build details
Source files are read by Ansible from apps/nas-admin/ via lookup('file', ...) and
injected as two ConfigMaps (nas-admin-source, nas-admin-templates).
A busybox initContainer copies them into a local-path workspace PVC before Kaniko runs.
The Dockerfile is the same file used for local docker build — no divergence between
local and cluster builds.
Estimated build time: ~2 min (python:3.12-slim + ~5 small packages).
RBAC
ClusterRole grants:
persistentvolumes— get, list, watch, delete (for orphaned PV cleanup)persistentvolumeclaims— get, list, watch, deletepods,events,storageclasses— get, list, watch only
Namespace
Deployed in namespace storage (created by the Ansible role if missing).
Gotchas
Cross-namespace PVC access
PVCs are namespace-scoped. The pod runs in storage but smb-nas-pvc lives in default — it cannot be mounted cross-namespace.
Fix: install-nas-admin creates a dedicated static PV (nas-admin-browse-pv) pointing to //192.168.178.102/service with nodeStageSecretRef: {name: smbcreds, namespace: default}, plus a matching PVC (nas-admin-nas-pvc) in the storage namespace. The smbcreds Secret lives in default (created by install-cifs-nas) but the CSI driver reads it directly — no cross-namespace issue.
Variables that control this: nas_admin_nas_ip (default 192.168.178.102), nas_admin_nas_share (default service), nas_admin_nas_pvc_name (default nas-admin-nas-pvc).
Forcing Helm to re-apply ClusterRole changes
Without helm-diff plugin installed, kubernetes.core.helm reports changed=0 and skips upgrades when no values changed. To force a re-apply after editing chart templates (e.g., RBAC verbs), bump version in Chart.yaml — Helm will detect a chart version change and do the upgrade.
Helm chart version vs image tag
The chart version (Chart.yaml) tracks chart structure changes. The image tag is always latest with pullPolicy: Always. After a code-only change (make nas-admin-build), run kubectl rollout restart deployment/nas-admin -n storage to pull the new image — Helm won't detect the rebuild.
Related skills
storage— CIFS/SMB CSI driver, PV/PVC patterns, StorageClass variantsgateway— HTTPRoute + shared Gateway pattern used by the HTTPRoute in this chart