// DeploymentEditStorage.tsx — 编辑器「存储挂载」Tab (M8) // 编辑内容:spec.template.spec.volumes[] — 数据卷定义 // 支持 6 种常用 volume 类型:emptyDir / hostPath / configMap / secret / persistentVolumeClaim / nfs // 作者: Logan import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Plus, Trash2, Save, Loader2, HardDrive, Database, Folder, Lock, } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { kubernetesApi } from '@/services/api'; interface Props { clusterCode: string; namespace: string; deploymentName: string; manifest: any | null; onSaved: () => void; onCancel: () => void; } /** 单个 volume 的编辑态 */ interface VolumeForm { name: string; type: 'emptyDir' | 'hostPath' | 'configMap' | 'secret' | 'persistentVolumeClaim' | 'nfs'; // 各类型字段(仅对应类型有效) medium?: string; // emptyDir.medium: "" | "Memory" sizeLimit?: string; // emptyDir.sizeLimit path?: string; // hostPath.path pathType?: string; // hostPath.pathType: Directory | File | Socket | ... configMapName?: string; // configMap.name secretName?: string; // secret.secretName pvcName?: string; // persistentVolumeClaim.claimName nfsServer?: string; // nfs.server nfsPath?: string; // nfs.path readOnly?: boolean; // pvc/nfs readOnly items?: string; // configMap/secret items (JSON 文本,高级用户) } /** 从 K8s volume 对象提取为 form */ function volumeToForm(v: any): VolumeForm { if (!v || typeof v !== 'object') return { name: '', type: 'emptyDir' }; const name = v.name || ''; if (v.emptyDir) { return { name, type: 'emptyDir', medium: v.emptyDir.medium || '', sizeLimit: v.emptyDir.sizeLimit || '', }; } if (v.hostPath) { return { name, type: 'hostPath', path: v.hostPath.path || '', pathType: v.hostPath.type || 'Directory', }; } if (v.configMap) { return { name, type: 'configMap', configMapName: v.configMap.name || '', items: v.configMap.items ? JSON.stringify(v.configMap.items) : '', }; } if (v.secret) { return { name, type: 'secret', secretName: v.secret.secretName || '', items: v.secret.items ? JSON.stringify(v.secret.items) : '', }; } if (v.persistentVolumeClaim) { return { name, type: 'persistentVolumeClaim', pvcName: v.persistentVolumeClaim.claimName || '', readOnly: !!v.persistentVolumeClaim.readOnly, }; } if (v.nfs) { return { name, type: 'nfs', nfsServer: v.nfs.server || '', nfsPath: v.nfs.path || '', readOnly: !!v.nfs.readOnly, }; } // 未识别类型 — fallback 为 emptyDir 占位 return { name, type: 'emptyDir' }; } /** 将 form 转回 K8s volume 对象 */ function formToVolume(f: VolumeForm): any { const v: any = { name: f.name.trim() }; if (!v.name) return null; // 无名 volume 跳过 switch (f.type) { case 'emptyDir': { const ed: any = {}; if (f.medium) ed.medium = f.medium; if (f.sizeLimit) ed.sizeLimit = f.sizeLimit; v.emptyDir = ed; break; } case 'hostPath': { v.hostPath = { path: f.path || '', type: f.pathType || 'Directory', }; break; } case 'configMap': { v.configMap = { name: f.configMapName || '' }; if (f.items) { try { v.configMap.items = JSON.parse(f.items); } catch {} } break; } case 'secret': { v.secret = { secretName: f.secretName || '' }; if (f.items) { try { v.secret.items = JSON.parse(f.items); } catch {} } break; } case 'persistentVolumeClaim': { v.persistentVolumeClaim = { claimName: f.pvcName || '', ...(f.readOnly ? { readOnly: true } : {}), }; break; } case 'nfs': { v.nfs = { server: f.nfsServer || '', path: f.nfsPath || '', ...(f.readOnly ? { readOnly: true } : {}), }; break; } } return v; } const TYPE_META: Record = { emptyDir: { label: 'emptyDir', icon: Folder, color: 'text-slate-300' }, hostPath: { label: 'hostPath', icon: HardDrive, color: 'text-amber-400' }, configMap: { label: 'configMap', icon: Folder, color: 'text-blue-400' }, secret: { label: 'secret', icon: Lock, color: 'text-rose-400' }, persistentVolumeClaim: { label: 'PVC', icon: Database, color: 'text-emerald-400' }, nfs: { label: 'NFS', icon: HardDrive, color: 'text-purple-400' }, }; export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) { const { toast } = useToast(); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(!manifest); const [volumes, setVolumes] = useState([]); const [rawManifest, setRawManifest] = useState(manifest || {}); // 容器已声明的 volumeMounts(只读展示,便于用户对齐 volume name) const [mountSummary, setMountSummary] = useState>([]); useEffect(() => { const m = manifest; if (!m) { setLoading(false); return; } setRawManifest(m); const podSpec = m?.spec?.template?.spec || {}; const vols: any[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : []; setVolumes(vols.map(volumeToForm)); // 汇总容器 volumeMounts const allContainers = [ ...(podSpec.containers || []), ...(podSpec.initContainers || []), ]; const summary = allContainers.map((c: any) => ({ container: c.name || '(unnamed)', mounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []) .map((vm: any) => `${vm.name}:${vm.mountPath}${vm.readOnly ? '(ro)' : ''}`), })).filter((s: any) => s.mounts.length > 0); setMountSummary(summary); setLoading(false); }, [manifest]); const addVolume = () => setVolumes(prev => [...prev, { name: '', type: 'emptyDir' }]); const removeVolume = (i: number) => setVolumes(prev => prev.filter((_, idx) => idx !== i)); const updateVolume = (i: number, patch: Partial) => setVolumes(prev => prev.map((v, idx) => (idx === i ? { ...v, ...patch } : v))); const save = async () => { setSaving(true); try { const base = JSON.parse(JSON.stringify(rawManifest)); base.spec = base.spec || {}; base.spec.template = base.spec.template || {}; base.spec.template.spec = base.spec.template.spec || {}; const k8sVols = volumes.map(formToVolume).filter(Boolean); if (k8sVols.length > 0) { base.spec.template.spec.volumes = k8sVols; } else { delete base.spec.template.spec.volumes; } const json = JSON.stringify(base, null, 2); await kubernetesApi.updateResourceYaml(clusterCode, namespace, 'deployments', deploymentName, json); toast({ title: '存储配置已保存', description: `${deploymentName} volumes 更新已提交` }); onSaved(); } catch (e: any) { toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setSaving(false); } }; if (loading) { return (

加载中...

); } if (!manifest) { return ( 未获取到 manifest 数据,请使用 YAML 编辑模式直接修改。 ); } return (
{/* 数据卷列表 */}
数据卷 (Volumes) {volumes.length}

Pod 级别的卷定义,容器通过 volumeMounts 引用这些卷名。

{volumes.length === 0 ? (
未配置数据卷,点击右上角「添加数据卷」
) : volumes.map((vol, i) => { const meta = TYPE_META[vol.type]; const Icon = meta.icon; return (
updateVolume(i, { name: e.target.value })} placeholder="volume-name" className="flex-1 h-8 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
{/* 类型相关字段 */} {vol.type === 'emptyDir' && (
updateVolume(i, { medium: e.target.value })} placeholder="" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
updateVolume(i, { sizeLimit: e.target.value })} placeholder="512Mi" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
)} {vol.type === 'hostPath' && (
updateVolume(i, { path: e.target.value })} placeholder="/data/app" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
)} {vol.type === 'configMap' && (
updateVolume(i, { configMapName: e.target.value })} placeholder="app-config" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> updateVolume(i, { items: e.target.value })} placeholder='[{"key":"app.conf","path":"app.conf"}]' className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
)} {vol.type === 'secret' && (
updateVolume(i, { secretName: e.target.value })} placeholder="app-tls" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" /> updateVolume(i, { items: e.target.value })} placeholder='[{"key":"tls.crt","path":"tls.crt"}]' className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
)} {vol.type === 'persistentVolumeClaim' && (
updateVolume(i, { pvcName: e.target.value })} placeholder="data-pvc" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
)} {vol.type === 'nfs' && (
updateVolume(i, { nfsServer: e.target.value })} placeholder="10.10.10.100" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
updateVolume(i, { nfsPath: e.target.value })} placeholder="/export/data" className="h-7 mt-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
)}
); })}
{/* 容器 volumeMounts 汇总(只读) */} {mountSummary.length > 0 && ( 容器挂载点 (volumeMounts)

容器侧已声明的挂载,请在「容器信息」Tab 修改。

{mountSummary.map((s, i) => (
{s.container}
{s.mounts.map((m, j) => ( {m} ))}
))}
)} {/* 操作栏 */}
); }