fix(k8s): Deployment 存储挂载六种卷类型 YAML 双向映射
- 抽取 deployment-volume-utils,修正 PVC/NFS/CM/Secret/HostPath/EmptyDir 读写 - 保存时保留系统卷与 serviceAccount 挂载,合并用户卷与挂载点 - 挂载支持 subPath;ConfigMap/Secret items 支持 key:path 简写
This commit is contained in:
1 parent
5fad2db0c6
commit
a8305de0f5
98 files changed
+453
-250
No files matched your search
@@ -12,6 +12,13 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { saveDeploymentManifest } from './deployment-manifest-save';
|
||||
import {
|
||||
type VolumeForm,
|
||||
type MountForm,
|
||||
parseStorageFromManifest,
|
||||
applyStorageToManifest,
|
||||
formToVolume,
|
||||
} from './deployment-volume-utils';
|
||||
|
||||
interface Props {
|
||||
clusterCode: string;
|
||||
@@ -22,33 +29,6 @@ interface Props {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
/** 数据卷定义 */
|
||||
interface VolumeForm {
|
||||
name: string;
|
||||
type: 'persistentVolumeClaim' | 'emptyDir' | 'hostPath' | 'nfs' | 'configMap' | 'secret';
|
||||
// PVC
|
||||
pvcName?: string; pvcReadOnly?: boolean;
|
||||
// EmptyDir
|
||||
medium?: string; sizeLimit?: string;
|
||||
// HostPath
|
||||
path?: string; pathType?: string;
|
||||
// NFS
|
||||
nfsServer?: string; nfsPath?: string; nfsReadOnly?: boolean;
|
||||
// ConfigMap
|
||||
configMapName?: string; cmItems?: string;
|
||||
// Secret
|
||||
secretName?: string; secItems?: string;
|
||||
}
|
||||
|
||||
/** 容器挂载点 */
|
||||
interface MountForm {
|
||||
containerName: string;
|
||||
volumeName: string;
|
||||
mountPath: string;
|
||||
subPath: string;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
const VOLUME_TYPES: { key: VolumeForm['type']; label: string; icon: typeof HardDrive; color: string; desc: string }[] = [
|
||||
{ key: 'persistentVolumeClaim', label: '存储卷声明', icon: Database, color: 'text-emerald-400', desc: 'PVC' },
|
||||
{ key: 'emptyDir', label: '临时目录', icon: Disc, color: 'text-slate-400', desc: 'EmptyDir' },
|
||||
@@ -58,32 +38,6 @@ const VOLUME_TYPES: { key: VolumeForm['type']; label: string; icon: typeof HardD
|
||||
{ key: 'secret', label: '密文', icon: Lock, color: 'text-rose-400', desc: 'Secret' },
|
||||
];
|
||||
|
||||
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 || '', 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.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 };
|
||||
return { name, type: 'emptyDir' };
|
||||
}
|
||||
|
||||
function formToVolume(f: VolumeForm): any {
|
||||
const v: Record<string, any> = { name: f.name.trim() };
|
||||
if (!v.name) return null;
|
||||
switch (f.type) {
|
||||
case 'emptyDir': { const ed: Record<string, 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.cmItems) try { v.configMap.items = JSON.parse(f.cmItems); } catch {} break; }
|
||||
case 'secret': { v.secret = { secretName: f.secretName || '' }; if (f.secItems) try { v.secret.items = JSON.parse(f.secItems); } catch {} break; }
|
||||
case 'persistentVolumeClaim': v.persistentVolumeClaim = { claimName: f.pvcName || '', ...(f.pvcReadOnly ? { readOnly: true } : {}) }; break;
|
||||
case 'nfs': v.nfs = { server: f.nfsServer || '', path: f.nfsPath || '', ...(f.nfsReadOnly ? { readOnly: true } : {}) }; break;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
export function DeploymentEditStorage({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) {
|
||||
const { toast } = useToast();
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -92,41 +46,17 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
||||
const [volumes, setVolumes] = useState<VolumeForm[]>([]);
|
||||
const [mounts, setMounts] = useState<MountForm[]>([]);
|
||||
const [containers, setContainers] = useState<string[]>([]);
|
||||
const [passthroughVolumes, setPassthroughVolumes] = useState<Record<string, any>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const m = manifest;
|
||||
if (!m) { setLoading(false); return; }
|
||||
setRawManifest(m);
|
||||
const podSpec = m?.spec?.template?.spec || {};
|
||||
|
||||
// 数据卷
|
||||
const vols: Record<string, any>[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : [];
|
||||
setVolumes(vols.map(volumeToForm));
|
||||
|
||||
// 容器列表
|
||||
const allContainers = [
|
||||
...(podSpec.containers || []),
|
||||
...(podSpec.initContainers || []),
|
||||
];
|
||||
const containerNames = allContainers.map(( c: any) => c.name || '(unnamed)');
|
||||
setContainers(containerNames);
|
||||
|
||||
// 挂载点
|
||||
const allMounts: MountForm[] = [];
|
||||
for (const c of allContainers) {
|
||||
const cName = c.name || '(unnamed)';
|
||||
const vms: Record<string, any>[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
|
||||
for (const vm of vms) {
|
||||
allMounts.push({
|
||||
containerName: cName,
|
||||
volumeName: vm.name || '',
|
||||
mountPath: vm.mountPath || '',
|
||||
subPath: vm.subPath || '',
|
||||
readOnly: !!vm.readOnly,
|
||||
});
|
||||
}
|
||||
}
|
||||
setMounts(allMounts);
|
||||
const parsed = parseStorageFromManifest(m);
|
||||
setVolumes(parsed.volumes);
|
||||
setMounts(parsed.mounts);
|
||||
setContainers(parsed.containers);
|
||||
setPassthroughVolumes(parsed.passthroughVolumes);
|
||||
setLoading(false);
|
||||
}, [manifest]);
|
||||
|
||||
@@ -147,35 +77,19 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
||||
setMounts(prev => prev.map((m, idx) => (idx === i ? { ...m, ...patch } : m)));
|
||||
|
||||
const save = async () => {
|
||||
for (const vol of volumes) {
|
||||
if (!vol.name.trim()) {
|
||||
toast({ title: '请填写数据卷名称', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!formToVolume(vol)) {
|
||||
toast({ title: '数据卷配置不完整', description: `请补全「${vol.name}」的类型必填项`, variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
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 podSpec = base.spec.template.spec;
|
||||
|
||||
// Save volumes
|
||||
const k8sVols = volumes.map(formToVolume).filter(Boolean);
|
||||
if (k8sVols.length > 0) podSpec.volumes = k8sVols;
|
||||
else delete podSpec.volumes;
|
||||
|
||||
// 分别更新工作容器和初始化容器的 volumeMounts // Logan.Li
|
||||
const applyMounts = (c: any) => {
|
||||
const cName = c.name || '(unnamed)';
|
||||
const cMounts = mounts
|
||||
.filter(m => m.containerName === cName && m.volumeName && m.mountPath)
|
||||
.map(m => {
|
||||
const vm: Record<string, any> = { name: m.volumeName, mountPath: m.mountPath, readOnly: m.readOnly };
|
||||
if (m.subPath) vm.subPath = m.subPath;
|
||||
return vm;
|
||||
});
|
||||
if (cMounts.length > 0) c.volumeMounts = cMounts;
|
||||
else delete c.volumeMounts;
|
||||
};
|
||||
(podSpec.containers || []).forEach(applyMounts);
|
||||
(podSpec.initContainers || []).forEach(applyMounts);
|
||||
|
||||
const base = applyStorageToManifest(rawManifest, volumes, mounts, passthroughVolumes);
|
||||
await saveDeploymentManifest(clusterCode, namespace, deploymentName, base);
|
||||
toast({ title: '存储配置已保存', description: `${deploymentName} 更新已提交` });
|
||||
onSaved(base);
|
||||
@@ -279,8 +193,13 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
||||
|
||||
{vol.type === 'emptyDir' && (
|
||||
<div className="flex items-center gap-2 pl-[88px]">
|
||||
<select value={vol.medium || ''} onChange={e => updateVolume(i, { medium: e.target.value })}
|
||||
className="h-7 w-28 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||
<option value="">默认磁盘</option>
|
||||
<option value="Memory">Memory</option>
|
||||
</select>
|
||||
<Input value={vol.sizeLimit || ''} onChange={e => updateVolume(i, { sizeLimit: e.target.value })}
|
||||
placeholder="sizeLimit (如 512Mi, 可为空)"
|
||||
placeholder="sizeLimit (如 512Mi)"
|
||||
className="h-7 flex-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
</div>
|
||||
)}
|
||||
@@ -323,7 +242,7 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
||||
placeholder="ConfigMap 名称"
|
||||
className="h-7 flex-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Input value={vol.cmItems || ''} onChange={e => updateVolume(i, { cmItems: e.target.value })}
|
||||
placeholder="KeyToPath (可为空)"
|
||||
placeholder="key:path 或 JSON"
|
||||
className="h-7 w-40 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
</div>
|
||||
)}
|
||||
@@ -335,7 +254,7 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
||||
placeholder="Secret 名称"
|
||||
className="h-7 flex-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Input value={vol.secItems || ''} onChange={e => updateVolume(i, { secItems: e.target.value })}
|
||||
placeholder="KeyToPath (可为空)"
|
||||
placeholder="key:path 或 JSON"
|
||||
className="h-7 w-40 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
</div>
|
||||
)}
|
||||
@@ -384,6 +303,9 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
|
||||
<Input value={m.mountPath} onChange={e => updateMount(i, { mountPath: e.target.value })}
|
||||
placeholder="挂载路径 /app/data"
|
||||
className="h-7 flex-1 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Input value={m.subPath} onChange={e => updateMount(i, { subPath: e.target.value })}
|
||||
placeholder="subPath (可选)"
|
||||
className="h-7 w-28 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
{/* 只读/读写 */}
|
||||
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-0.5 border border-slate-700">
|
||||
<button onClick={() => updateMount(i, { readOnly: false })}
|
||||
|
||||
@@ -100,18 +100,8 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
|
||||
const loadManifest = useCallback(async () => {
|
||||
try {
|
||||
const yamlText = await kubernetesApi.getResourceYaml(clusterCode, namespace, 'deployments', deploymentName);
|
||||
// 简易 YAML→JSON:交给浏览器原生 JSON.parse 兜底(K8s API 多以 JSON 返回);
|
||||
// 若是纯 YAML,则尝试用 js-yaml(如项目已安装),否则保留为 null 让编辑模式降级。
|
||||
let parsed: Record<string, any> = null;
|
||||
try { parsed = JSON.parse(yamlText); } catch {
|
||||
// 非纯 JSON,尝试动态加载 js-yaml
|
||||
try {
|
||||
const jsyaml: Record<string, any> = await import('js-yaml');
|
||||
parsed = jsyaml.load(yamlText);
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
}
|
||||
const { parseEditableManifest } = await import('./k8s-manifest-utils');
|
||||
const { manifest: parsed } = await parseEditableManifest(yamlText);
|
||||
setManifest(parsed || deployment);
|
||||
} catch (e) {
|
||||
console.warn('加载 manifest 失败(编辑功能降级):', e);
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
// 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<string, any>;
|
||||
}
|
||||
|
||||
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<string, any>): 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<string, any>): 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<string, any> | 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<string, any> = { name: volName };
|
||||
switch (f.type) {
|
||||
case 'emptyDir': {
|
||||
const ed: Record<string, any> = {};
|
||||
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<string, any> = { 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<string, any> = { 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<string, any> = { 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<string, any> = { 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<string, any> = { server, path };
|
||||
if (f.nfsReadOnly) nfs.readOnly = true;
|
||||
v.nfs = nfs;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
export function mountToForm(containerName: string, vm: Record<string, any>): MountForm {
|
||||
return {
|
||||
containerName,
|
||||
volumeName: vm.name || '',
|
||||
mountPath: vm.mountPath || '',
|
||||
subPath: vm.subPath || '',
|
||||
readOnly: !!vm.readOnly,
|
||||
};
|
||||
}
|
||||
|
||||
export function formToMount(m: MountForm): Record<string, any> | null {
|
||||
if (!m.volumeName?.trim() || !m.mountPath?.trim()) return null;
|
||||
const vm: Record<string, any> = { 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<string, any>, passthroughVolumeNames: Set<string>): 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<string, any>): {
|
||||
volumes: VolumeForm[];
|
||||
mounts: MountForm[];
|
||||
containers: string[];
|
||||
passthroughVolumes: Record<string, any>[];
|
||||
} {
|
||||
const podSpec = manifest?.spec?.template?.spec || {};
|
||||
const rawVols: Record<string, any>[] = 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<string, any>[] = 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<string, any>,
|
||||
volumes: VolumeForm[],
|
||||
mounts: MountForm[],
|
||||
passthroughVolumes: Record<string, any>[],
|
||||
): Record<string, any> {
|
||||
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<string, any>[];
|
||||
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<string, any>) => {
|
||||
const cName = c.name || '(unnamed)';
|
||||
const existing: Record<string, any>[] = 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<string, any>[];
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user