/** * TnsCsiDataContext — shared data provider for tns-csi Kubernetes resources. * * Wraps the K8s hook calls and provides filtered tns-csi resources to all * child pages through React context, avoiding prop drilling and duplicate * API calls. */ import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { CSIDriver, filterTnsCsiPersistentVolumes, filterTnsCsiPVCs, filterTnsCsiStorageClasses, filterTnsCsiVolumeSnapshots, isKubeList, isTnsCsiVolumeSnapshotClass, TNS_CSI_PROVISIONER, TnsCsiPersistentVolume, TnsCsiPersistentVolumeClaim, TnsCsiPod, TnsCsiStorageClass, VolumeSnapshot, VolumeSnapshotClass, } from './k8s'; import { fetchTruenasPoolStats, getTnsCsiConfig, PoolStats } from './truenas'; // --------------------------------------------------------------------------- // Context shape // --------------------------------------------------------------------------- export interface TnsCsiContextValue { // Driver presence csiDriver: CSIDriver | null; driverInstalled: boolean; // Core resources (filtered to tns-csi only) storageClasses: TnsCsiStorageClass[]; persistentVolumes: TnsCsiPersistentVolume[]; persistentVolumeClaims: TnsCsiPersistentVolumeClaim[]; // Driver pods controllerPods: TnsCsiPod[]; nodePods: TnsCsiPod[]; // Snapshots (CRD — may be unavailable) volumeSnapshots: VolumeSnapshot[]; 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; // Manual refresh trigger refresh: () => void; } // --------------------------------------------------------------------------- // Context // --------------------------------------------------------------------------- const TnsCsiContext = createContext(null); export function useTnsCsiContext(): TnsCsiContextValue { const ctx = useContext(TnsCsiContext); if (!ctx) { throw new Error('useTnsCsiContext must be used within a TnsCsiDataProvider'); } return ctx; } // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- export function TnsCsiDataProvider({ children }: { children: React.ReactNode }) { // K8s resource hooks — headlamp re-fetches on cluster changes automatically const [allStorageClasses, scError] = K8s.ResourceClasses.StorageClass.useList(); const [allPvs, pvError] = K8s.ResourceClasses.PersistentVolume.useList(); const [allPvcs, pvcError] = K8s.ResourceClasses.PersistentVolumeClaim.useList({ namespace: '' }); // Pods fetched via label selector through ApiProxy (useList doesn't support selectors easily) const [controllerPods, setControllerPods] = useState([]); const [nodePods, setNodePods] = useState([]); const [csiDriver, setCsiDriver] = useState(null); const [volumeSnapshots, setVolumeSnapshots] = useState([]); const [volumeSnapshotClasses, setVolumeSnapshotClasses] = useState([]); const [snapshotCrdAvailable, setSnapshotCrdAvailable] = useState(false); const [asyncLoading, setAsyncLoading] = useState(true); const [asyncError, setAsyncError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); const [poolStats, setPoolStats] = useState([]); const [poolStatsError, setPoolStatsError] = useState(null); const refresh = useCallback(() => { setRefreshKey(k => k + 1); }, []); useEffect(() => { let cancelled = false; async function fetchAsync() { setAsyncLoading(true); setAsyncError(null); try { // CSIDriver try { const driver = (await ApiProxy.request( `/apis/storage.k8s.io/v1/csidrivers/${TNS_CSI_PROVISIONER}` )) as CSIDriver; if (!cancelled) setCsiDriver(driver); } catch { if (!cancelled) setCsiDriver(null); } // Controller pods try { const ctrlList = await ApiProxy.request( `/api/v1/namespaces/kube-system/pods?labelSelector=${encodeURIComponent( 'app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=controller' )}` ); if (!cancelled && isKubeList(ctrlList)) { setControllerPods(ctrlList.items as TnsCsiPod[]); } } catch { if (!cancelled) setControllerPods([]); } // Node pods try { const nodeList = await ApiProxy.request( `/api/v1/namespaces/kube-system/pods?labelSelector=${encodeURIComponent( 'app.kubernetes.io/name=tns-csi-driver,app.kubernetes.io/component=node' )}` ); if (!cancelled && isKubeList(nodeList)) { setNodePods(nodeList.items as TnsCsiPod[]); } } catch { if (!cancelled) setNodePods([]); } // VolumeSnapshots (CRD — graceful degradation) try { const vscList = await ApiProxy.request( '/apis/snapshot.storage.k8s.io/v1/volumesnapshotclasses' ); if (!cancelled && isKubeList(vscList)) { const allSnapshotClasses = vscList.items as VolumeSnapshotClass[]; const tnsCsiSnapshotClasses = allSnapshotClasses.filter(isTnsCsiVolumeSnapshotClass); setVolumeSnapshotClasses(tnsCsiSnapshotClasses); setSnapshotCrdAvailable(true); const tnsCsiClassNames = new Set(tnsCsiSnapshotClasses.map(c => c.metadata.name)); const vsList = await ApiProxy.request( '/apis/snapshot.storage.k8s.io/v1/volumesnapshots' ); if (!cancelled && isKubeList(vsList)) { const allSnapshots = vsList.items as VolumeSnapshot[]; setVolumeSnapshots(filterTnsCsiVolumeSnapshots(allSnapshots, tnsCsiClassNames)); } } } catch { if (!cancelled) { setSnapshotCrdAvailable(false); setVolumeSnapshotClasses([]); 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)); } } finally { if (!cancelled) setAsyncLoading(false); } } void fetchAsync(); return () => { cancelled = true; }; }, [refreshKey]); // --------------------------------------------------------------------------- // 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(extractJsonData(allStorageClasses as unknown[])); }, [allStorageClasses]); const persistentVolumes = useMemo(() => { if (!allPvs) return []; return filterTnsCsiPersistentVolumes(extractJsonData(allPvs as unknown[])); }, [allPvs]); const persistentVolumeClaims = useMemo(() => { if (!allPvcs || persistentVolumes.length === 0) return []; return filterTnsCsiPVCs( extractJsonData(allPvcs as unknown[]) as TnsCsiPersistentVolumeClaim[], persistentVolumes ); }, [allPvcs, persistentVolumes]); // --------------------------------------------------------------------------- // Combined loading / error state // --------------------------------------------------------------------------- const loading = asyncLoading || !allStorageClasses || !allPvs || !allPvcs; const errors: string[] = []; if (scError) errors.push(String(scError)); if (pvError) errors.push(String(pvError)); if (pvcError) errors.push(String(pvcError)); if (asyncError) errors.push(asyncError); const error = errors.length > 0 ? errors.join('; ') : null; const driverInstalled = csiDriver !== null; // --------------------------------------------------------------------------- // Memoized context value to prevent unnecessary re-renders // --------------------------------------------------------------------------- const value = useMemo( () => ({ csiDriver, driverInstalled, storageClasses, persistentVolumes, persistentVolumeClaims, controllerPods, nodePods, volumeSnapshots, volumeSnapshotClasses, snapshotCrdAvailable, poolStats, poolStatsError, loading, error, refresh, }), [ csiDriver, driverInstalled, storageClasses, persistentVolumes, persistentVolumeClaims, controllerPods, nodePods, volumeSnapshots, volumeSnapshotClasses, snapshotCrdAvailable, poolStats, poolStatsError, loading, error, refresh, ] ); return {children}; }