// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8) // Logan.Li // 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / DNS / 安全上下文 / 终止宽限期 / 调度器 / SA // 作者: 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 { Save, Loader2, Settings, RefreshCw, Shield, Clock, Network, Plus, Trash2, } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { kubernetesApi } from '@/api'; import { saveDeploymentManifest } from './deployment-manifest-save'; import { SchedulingSection, type ScheduleMode } from './deployment-edit-advanced/SchedulingSection'; import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection'; import type { JsonData } from '@/types/common'; interface Props { clusterCode: string; namespace: string; deploymentName: string; manifest: JsonData | null; onSaved: (updated?: JsonData) => void; onCancel: () => void; } interface Toleration { key: string; operator: string; // Equal | Exists value: string; effect: string; // NoSchedule | PreferNoSchedule | NoExecute tolerationSeconds: string; } export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) { const { toast } = useToast(); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(!manifest); const [rawManifest, setRawManifest] = useState(manifest || {}); // 更新策略 const [strategyType, setStrategyType] = useState<'RollingUpdate' | 'Recreate'>('RollingUpdate'); const [maxSurge, setMaxSurge] = useState('25%'); const [maxUnavailable, setMaxUnavailable] = useState('25%'); // 历史/进度 const [revisionHistoryLimit, setRevisionHistoryLimit] = useState('10'); const [progressDeadlineSeconds, setProgressDeadlineSeconds] = useState('600'); // ──── 调度策略 ──── const [scheduleMode, setScheduleMode] = useState('nodeSelector'); const [nodeName, setNodeName] = useState(''); const [availableNodes, setAvailableNodes] = useState }>>([]); const [nodesLoading, setNodesLoading] = useState(false); const [labelSelectorKv, setLabelSelectorKv] = useState>([]); const [nodeLabels, setNodeLabels] = useState>({}); const [tolerations, setTolerations] = useState([]); const [dnsPolicy, setDnsPolicy] = useState('ClusterFirst'); const [restartPolicy, setRestartPolicy] = useState('Always'); const [schedulerName, setSchedulerName] = useState('default-scheduler'); const [terminationGracePeriodSeconds, setTerminationGracePeriodSeconds] = useState('30'); const [serviceAccountName, setServiceAccountName] = useState(''); // Pod 安全上下文 const [podFsGroup, setPodFsGroup] = useState(''); const [podRunAsUser, setPodRunAsUser] = useState(''); const [podRunAsGroup, setPodRunAsGroup] = useState(''); const [podRunAsNonRoot, setPodRunAsNonRoot] = useState(false); // 主机设置(M8 补全) const [hostNetwork, setHostNetwork] = useState(false); const [hostIPC, setHostIPC] = useState(false); const [hostPID, setHostPID] = useState(false); const [shareProcessNamespace, setShareProcessNamespace] = useState(false); // hostAliases(M8 补全) const [hostAliases, setHostAliases] = useState>([]); // dnsConfig(M8 补全) const [dnsNameservers, setDnsNameservers] = useState(''); const [dnsSearches, setDnsSearches] = useState(''); const [dnsOptions, setDnsOptions] = useState>([]); useEffect(() => { const m = manifest; if (!m) { setLoading(false); return; } setRawManifest(m); const spec = m?.spec || {}; const podSpec = spec?.template?.spec || {}; // 更新策略 const strat = spec.strategy || {}; setStrategyType(strat.type || 'RollingUpdate'); const ru = strat.rollingUpdate || {}; setMaxSurge(String(ru.maxSurge ?? '25%')); setMaxUnavailable(String(ru.maxUnavailable ?? '25%')); // 历史/进度 setRevisionHistoryLimit(spec.revisionHistoryLimit != null ? String(spec.revisionHistoryLimit) : '10'); setProgressDeadlineSeconds(spec.progressDeadlineSeconds != null ? String(spec.progressDeadlineSeconds) : '600'); // nodeSelector → labelSelectorKv const ns = podSpec.nodeSelector || {}; const nsEntries = Object.entries(ns).map(([k, v]) => ({ key: k, value: String(v) })); setLabelSelectorKv(nsEntries); // nodeName setNodeName(podSpec.nodeName || ''); // Determine scheduleMode if (podSpec.nodeName) { setScheduleMode('nodeName'); } else if (podSpec.affinity?.nodeAffinity) { setScheduleMode('nodeAffinity'); } else if (nsEntries.length > 0) { setScheduleMode('nodeSelector'); } // 加载节点列表 if (clusterCode) { setNodesLoading(true); kubernetesApi.getClusterNodes(clusterCode).then((res: JsonData) => { const nodes: JsonData[] = Array.isArray(res) ? res : (res?.items || res?.list || []); const mapped = nodes.map((n: JsonData) => { const meta = n.metadata || {}; const status = n.status || {}; const addresses: JsonData[] = Array.isArray(status.addresses) ? status.addresses : []; const internalIP = addresses.find((a: JsonData) => a.type === 'InternalIP')?.address || ''; return { name: meta.name || '', ip: internalIP, labels: meta.labels || {} }; }); setAvailableNodes(mapped); // 聚合标签 const lblMap: Record = {}; for (const n of mapped) { for (const [k, v] of Object.entries(n.labels)) { const sv = String(v); if (!lblMap[k]) lblMap[k] = []; if (!lblMap[k].includes(sv)) lblMap[k].push(sv); } } setNodeLabels(lblMap); }).catch(() => {}).finally(() => setNodesLoading(false)); } // tolerations const tols: JsonData[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : []; setTolerations(tols.map(t => ({ key: t.key || '', operator: t.operator || 'Equal', value: t.value || '', effect: t.effect || '', tolerationSeconds: t.tolerationSeconds != null ? String(t.tolerationSeconds) : '', }))); setDnsPolicy(podSpec.dnsPolicy || 'ClusterFirst'); setRestartPolicy(podSpec.restartPolicy || 'Always'); setSchedulerName(podSpec.schedulerName || 'default-scheduler'); setTerminationGracePeriodSeconds(podSpec.terminationGracePeriodSeconds != null ? String(podSpec.terminationGracePeriodSeconds) : '30'); setServiceAccountName(podSpec.serviceAccountName || ''); // Pod securityContext const sc = podSpec.securityContext || {}; setPodFsGroup(sc.fsGroup != null ? String(sc.fsGroup) : ''); setPodRunAsUser(sc.runAsUser != null ? String(sc.runAsUser) : ''); setPodRunAsGroup(sc.runAsGroup != null ? String(sc.runAsGroup) : ''); setPodRunAsNonRoot(!!sc.runAsNonRoot); // 主机设置(M8 补全) setHostNetwork(!!podSpec.hostNetwork); setHostIPC(!!podSpec.hostIPC); setHostPID(!!podSpec.hostPID); setShareProcessNamespace(!!podSpec.shareProcessNamespace); // hostAliases(M8 补全) const ha: JsonData[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : []; setHostAliases(ha.map(a => ({ ip: a.ip || '', hostnames: Array.isArray(a.hostnames) ? a.hostnames.join(', ') : '', }))); // dnsConfig(M8 补全) const dc = podSpec.dnsConfig || {}; setDnsNameservers(Array.isArray(dc.nameservers) ? dc.nameservers.join('\n') : ''); setDnsSearches(Array.isArray(dc.searches) ? dc.searches.join('\n') : ''); const opts: JsonData[] = Array.isArray(dc.options) ? dc.options : []; setDnsOptions(opts.map(o => ({ name: o.name || '', value: o.value || '' }))); setLoading(false); }, [manifest]); // labelSelector 操作 const addNs = () => setLabelSelectorKv(prev => [...prev, { key: '', value: '' }]); const removeNs = (i: number) => setLabelSelectorKv(prev => prev.filter((_, idx) => idx !== i)); const updateNs = (i: number, patch: Partial<{ key: string; value: string }>) => setLabelSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n))); 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 podSpec = base.spec.template.spec; // 更新策略 base.spec.strategy = { type: strategyType }; if (strategyType === 'RollingUpdate') { base.spec.strategy.rollingUpdate = { maxSurge, maxUnavailable }; } // 历史/进度 if (revisionHistoryLimit) base.spec.revisionHistoryLimit = Number(revisionHistoryLimit); if (progressDeadlineSeconds) base.spec.progressDeadlineSeconds = Number(progressDeadlineSeconds); // nodeSelector / nodeName / nodeAffinity if (scheduleMode === 'nodeName' && nodeName.trim()) { podSpec.nodeName = nodeName.trim(); delete podSpec.nodeSelector; if (podSpec.affinity) delete podSpec.affinity.nodeAffinity; } else if (scheduleMode === 'nodeSelector') { const nsObj: Record = {}; for (const { key, value } of labelSelectorKv) { const k = key.trim(); if (k) nsObj[k] = value; } if (Object.keys(nsObj).length > 0) podSpec.nodeSelector = nsObj; else delete podSpec.nodeSelector; if (podSpec.nodeName) delete podSpec.nodeName; if (podSpec.affinity) delete podSpec.affinity.nodeAffinity; } else if (scheduleMode === 'nodeAffinity') { const matchExpressions: JsonData[] = labelSelectorKv .filter(s => s.key.trim()) .map(s => ({ key: s.key.trim(), operator: 'In', values: [s.value] })); if (matchExpressions.length > 0) { podSpec.affinity = podSpec.affinity || {}; podSpec.affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchExpressions }], }, }; } else if (podSpec.affinity) { delete podSpec.affinity.nodeAffinity; } delete podSpec.nodeSelector; if (podSpec.nodeName) delete podSpec.nodeName; } // tolerations const tolsK8s = tolerations .filter(t => t.key || t.operator === 'Exists') .map(t => { const out: JsonData = {}; if (t.key) out.key = t.key; if (t.operator) out.operator = t.operator; if (t.value && t.operator !== 'Exists') out.value = t.value; if (t.effect) out.effect = t.effect; if (t.tolerationSeconds && t.effect === 'NoExecute') out.tolerationSeconds = Number(t.tolerationSeconds); return out; }); if (tolsK8s.length > 0) podSpec.tolerations = tolsK8s; else delete podSpec.tolerations; // 其他 Pod 级别 if (dnsPolicy) podSpec.dnsPolicy = dnsPolicy; if (restartPolicy) podSpec.restartPolicy = restartPolicy; if (schedulerName) podSpec.schedulerName = schedulerName; if (terminationGracePeriodSeconds) podSpec.terminationGracePeriodSeconds = Number(terminationGracePeriodSeconds); if (serviceAccountName) podSpec.serviceAccountName = serviceAccountName; // Pod 安全上下文 const sc: JsonData = {}; if (podFsGroup) sc.fsGroup = Number(podFsGroup); if (podRunAsUser) sc.runAsUser = Number(podRunAsUser); if (podRunAsGroup) sc.runAsGroup = Number(podRunAsGroup); if (podRunAsNonRoot) sc.runAsNonRoot = true; if (Object.keys(sc).length > 0) podSpec.securityContext = sc; else delete podSpec.securityContext; // 主机设置(M8 补全)— 只在为 true 时写入,false 删除字段 if (hostNetwork) podSpec.hostNetwork = true; else delete podSpec.hostNetwork; if (hostIPC) podSpec.hostIPC = true; else delete podSpec.hostIPC; if (hostPID) podSpec.hostPID = true; else delete podSpec.hostPID; if (shareProcessNamespace) podSpec.shareProcessNamespace = true; else delete podSpec.shareProcessNamespace; // hostAliases(M8 补全) const haK8s = hostAliases .filter(a => a.ip.trim()) .map(a => ({ ip: a.ip.trim(), hostnames: a.hostnames.split(/[,\s]+/).map((s: string) => s.trim()).filter(Boolean), })) .filter(a => a.hostnames.length > 0); if (haK8s.length > 0) podSpec.hostAliases = haK8s; else delete podSpec.hostAliases; // dnsConfig(M8 补全)— 仅 dnsPolicy=None 时 dnsConfig 才有意义,但仍允许编辑 const dcObj: JsonData = {}; const ns = dnsNameservers.split('\n').map(s => s.trim()).filter(Boolean); if (ns.length > 0) dcObj.nameservers = ns; const sr = dnsSearches.split('\n').map(s => s.trim()).filter(Boolean); if (sr.length > 0) dcObj.searches = sr; const dkOpts = dnsOptions.filter(o => o.name.trim()).map(o => { const out: JsonData = { name: o.name.trim() }; if (o.value.trim()) out.value = o.value.trim(); return out; }); if (dkOpts.length > 0) dcObj.options = dkOpts; if (Object.keys(dcObj).length > 0) podSpec.dnsConfig = dcObj; else delete podSpec.dnsConfig; await saveDeploymentManifest(clusterCode, namespace, deploymentName, base); toast({ title: '高级设置已保存', description: `${deploymentName} 更新已提交` }); onSaved(base); } catch (e) { toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' }); } finally { setSaving(false); } }; if (loading) { return (

加载中...

); } if (!manifest) { return ( 未获取到 manifest 数据,请使用 YAML 编辑模式直接修改。 ); } return (
{/* 更新策略 */} 更新策略 (Strategy)
{strategyType === 'RollingUpdate' && (
setMaxSurge(e.target.value)} placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" />
setMaxUnavailable(e.target.value)} placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" />
)}
{/* 历史版本 / 进度 */} 历史版本 & 进度
setRevisionHistoryLimit(e.target.value)} className="h-8 mt-1 dg-input text-sm font-mono" />
setProgressDeadlineSeconds(e.target.value)} className="h-8 mt-1 dg-input text-sm font-mono" />
{/* 容忍 (Tolerations) — 此前仅有状态无 UI // Logan.Li */}
容忍 (Tolerations)
{tolerations.length === 0 &&

未配置容忍

} {tolerations.map((t, i) => (
setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, key: e.target.value } : x))} className="h-7 dg-input text-xs font-mono" /> setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, value: e.target.value } : x))} className="h-7 dg-input text-xs font-mono" /> setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, tolerationSeconds: e.target.value } : x))} className="h-7 dg-input text-xs font-mono" />
))}
{/* Pod 级别杂项 */} Pod 级别
setSchedulerName(e.target.value)} className="h-8 mt-1 dg-input text-sm font-mono" />
setTerminationGracePeriodSeconds(e.target.value)} className="h-8 mt-1 dg-input text-sm font-mono" />
setServiceAccountName(e.target.value)} placeholder="default" className="h-8 mt-1 dg-input text-sm font-mono" />
{/* Pod 安全上下文 */} Pod 安全上下文 (securityContext)
setPodFsGroup(e.target.value)} placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
setPodRunAsUser(e.target.value)} placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
setPodRunAsGroup(e.target.value)} placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
{/* 主机设置(M8 补全) */} 主机设置 (Host Namespace) {/* 操作栏 */}
); }