// Deployment Pod 数据卷 ↔ K8s YAML 双向映射 // Logan.Li export type VolumeType = | 'persistentVolumeClaim' | 'emptyDir' | 'hostPath' | 'nfs' | 'configMap' | 'secret' | 'custom'; export interface VolumeForm { name: string; type: VolumeType; pvcName?: string; pvcReadOnly?: boolean; medium?: string; sizeLimit?: string; path?: string; pathType?: string; nfsServer?: string; nfsPath?: string; nfsReadOnly?: boolean; configMapName?: string; cmItems?: string; secretName?: string; secItems?: string; /** 未识别卷类型时保留原始对象,避免保存丢失 // Logan.Li */ _raw?: Record; } export interface MountForm { containerName: string; volumeName: string; mountPath: string; subPath: string; readOnly: boolean; } const SYSTEM_VOLUME_PREFIXES = ['kube-api-access-', 'default-token-', 'kube-']; /** 是否系统/不可编辑卷(保存时保留)// Logan.Li */ export function isPassthroughVolume(v: Record): boolean { if (!v || typeof v !== 'object') return false; const name = String(v.name || ''); if (SYSTEM_VOLUME_PREFIXES.some((p) => name.startsWith(p))) return true; return !!(v.projected || v.downwardAPI || v.csi || v.ephemeral); } /** 解析 KeyToPath:支持 JSON 或 key:path,key:path // Logan.Li */ export function parseKeyToPathItems(text?: string): Array<{ key: string; path: string }> | undefined { const raw = text?.trim(); if (!raw) return undefined; if (raw.startsWith('[')) { try { const arr = JSON.parse(raw); if (Array.isArray(arr)) return arr.filter((i) => i?.key && i?.path); } catch { /* fallthrough */ } } const items = raw.split(',').map((s) => s.trim()).filter(Boolean).map((pair) => { const idx = pair.indexOf(':'); if (idx <= 0) return null; return { key: pair.slice(0, idx).trim(), path: pair.slice(idx + 1).trim() }; }).filter(Boolean) as Array<{ key: string; path: string }>; return items.length ? items : undefined; } /** K8s volume → 表单 // Logan.Li */ export function volumeToForm(v: Record): VolumeForm { if (!v || typeof v !== 'object') return { name: '', type: 'emptyDir' }; const name = v.name || ''; if (v.persistentVolumeClaim) { return { name, type: 'persistentVolumeClaim', pvcName: v.persistentVolumeClaim.claimName || '', pvcReadOnly: !!v.persistentVolumeClaim.readOnly, }; } if (v.nfs) { return { name, type: 'nfs', nfsServer: v.nfs.server || '', nfsPath: v.nfs.path || '', nfsReadOnly: !!v.nfs.readOnly, }; } if (v.configMap) { return { name, type: 'configMap', configMapName: v.configMap.name || '', cmItems: v.configMap.items ? JSON.stringify(v.configMap.items) : '', }; } if (v.secret) { return { name, type: 'secret', secretName: v.secret.secretName || '', secItems: v.secret.items ? JSON.stringify(v.secret.items) : '', }; } if (v.hostPath) { return { name, type: 'hostPath', path: v.hostPath.path || '', pathType: v.hostPath.type || '', }; } if (v.emptyDir != null) { return { name, type: 'emptyDir', medium: v.emptyDir.medium || '', sizeLimit: v.emptyDir.sizeLimit || '', }; } if (isPassthroughVolume(v)) { return { name, type: 'custom', _raw: v }; } return { name, type: 'custom', _raw: v }; } /** 表单 → K8s volume // Logan.Li */ export function formToVolume(f: VolumeForm): Record | null { if (f.type === 'custom' && f._raw) return JSON.parse(JSON.stringify(f._raw)); const volName = f.name?.trim(); if (!volName) return null; const v: Record = { name: volName }; switch (f.type) { case 'emptyDir': { const ed: Record = {}; if (f.medium?.trim()) ed.medium = f.medium.trim(); if (f.sizeLimit?.trim()) ed.sizeLimit = f.sizeLimit.trim(); v.emptyDir = ed; break; } case 'hostPath': { const hp: Record = { path: f.path?.trim() || '/' }; if (f.pathType?.trim()) hp.type = f.pathType.trim(); v.hostPath = hp; break; } case 'configMap': { const cmName = f.configMapName?.trim(); if (!cmName) return null; const cm: Record = { name: cmName }; const items = parseKeyToPathItems(f.cmItems); if (items) cm.items = items; v.configMap = cm; break; } case 'secret': { const secName = f.secretName?.trim(); if (!secName) return null; const sec: Record = { secretName: secName }; const items = parseKeyToPathItems(f.secItems); if (items) sec.items = items; v.secret = sec; break; } case 'persistentVolumeClaim': { const claim = f.pvcName?.trim(); if (!claim) return null; const pvc: Record = { claimName: claim }; if (f.pvcReadOnly) pvc.readOnly = true; v.persistentVolumeClaim = pvc; break; } case 'nfs': { const server = f.nfsServer?.trim(); const path = f.nfsPath?.trim(); if (!server || !path) return null; const nfs: Record = { server, path }; if (f.nfsReadOnly) nfs.readOnly = true; v.nfs = nfs; break; } default: return null; } return v; } export function mountToForm(containerName: string, vm: Record): MountForm { return { containerName, volumeName: vm.name || '', mountPath: vm.mountPath || '', subPath: vm.subPath || '', readOnly: !!vm.readOnly, }; } export function formToMount(m: MountForm): Record | null { if (!m.volumeName?.trim() || !m.mountPath?.trim()) return null; const vm: Record = { name: m.volumeName.trim(), mountPath: m.mountPath.trim() }; if (m.subPath?.trim()) vm.subPath = m.subPath.trim(); if (m.readOnly) vm.readOnly = true; return vm; } const SYSTEM_MOUNT_PREFIXES = ['/var/run/secrets/kubernetes.io/serviceaccount']; export function isPassthroughMount(vm: Record, passthroughVolumeNames: Set): boolean { if (!vm) return false; if (passthroughVolumeNames.has(vm.name)) return true; const mp = vm.mountPath || ''; return SYSTEM_MOUNT_PREFIXES.some((p) => mp.startsWith(p)); } /** 从 manifest 解析卷与挂载表单 // Logan.Li */ export function parseStorageFromManifest(manifest: Record): { volumes: VolumeForm[]; mounts: MountForm[]; containers: string[]; passthroughVolumes: Record[]; } { const podSpec = manifest?.spec?.template?.spec || {}; const rawVols: Record[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : []; const passthroughVolumes = rawVols.filter(isPassthroughVolume); const editableVols = rawVols.filter((v) => !isPassthroughVolume(v)); const allContainers = [ ...(Array.isArray(podSpec.containers) ? podSpec.containers : []), ...(Array.isArray(podSpec.initContainers) ? podSpec.initContainers : []), ]; const containers = allContainers.map((c) => c.name || '(unnamed)'); const passthroughNames = new Set(passthroughVolumes.map((v) => v.name)); const mounts: MountForm[] = []; for (const c of allContainers) { const cName = c.name || '(unnamed)'; const vms: Record[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : []; for (const vm of vms) { if (isPassthroughMount(vm, passthroughNames)) continue; mounts.push(mountToForm(cName, vm)); } } return { volumes: editableVols.map(volumeToForm), mounts, containers, passthroughVolumes, }; } /** 将表单写回 manifest.spec.template.spec // Logan.Li */ export function applyStorageToManifest( manifest: Record, volumes: VolumeForm[], mounts: MountForm[], passthroughVolumes: Record[], ): Record { const base = JSON.parse(JSON.stringify(manifest)); base.spec = base.spec || {}; base.spec.template = base.spec.template || {}; base.spec.template.spec = base.spec.template.spec || {}; const podSpec = base.spec.template.spec; const userVols = volumes.map(formToVolume).filter(Boolean) as Record[]; const mergedVols = [...passthroughVolumes, ...userVols]; if (mergedVols.length > 0) podSpec.volumes = mergedVols; else delete podSpec.volumes; const passthroughNames = new Set(passthroughVolumes.map((v) => v.name)); const applyMounts = (c: Record) => { const cName = c.name || '(unnamed)'; const existing: Record[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : []; const kept = existing.filter((vm) => isPassthroughMount(vm, passthroughNames)); const user = mounts .filter((m) => m.containerName === cName) .map(formToMount) .filter(Boolean) as Record[]; const merged = [...kept, ...user]; if (merged.length > 0) c.volumeMounts = merged; else delete c.volumeMounts; }; (podSpec.containers || []).forEach(applyMounts); (podSpec.initContainers || []).forEach(applyMounts); return base; }