From b4591de97cf47fb8ef0468230b5a492396ab88ab Mon Sep 17 00:00:00 2001 From: attacker Date: Wed, 8 Jul 2026 20:37:03 +0800 Subject: [PATCH] =?UTF-8?q?refactor(quality):=20Q21=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=B7=A8=E5=9E=8B=E7=BB=84=E4=BB=B6=E6=8B=86=E5=88=86=20?= =?UTF-8?q?=E2=80=94=20DeploymentEditAdvanced=20590L=E2=86=92289L=20(-51%)?= =?UTF-8?q?,=20=E6=8F=90=E5=8F=96=20types(parseAdvancedManifest+buildAdvan?= =?UTF-8?q?cedManifest=20=E7=BA=AF=E5=87=BD=E6=95=B0)=20+=20UpdateStrategy?= =?UTF-8?q?Card/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCa?= =?UTF-8?q?rd=205=20=E4=B8=AA=E5=8F=97=E6=8E=A7=20Card=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6,=20manifest=20=E8=A7=A3=E6=9E=90(106L)=E5=92=8C?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96(121L)=E6=8F=90=E5=8F=96=E4=B8=BA?= =?UTF-8?q?=E7=BA=AF=E5=87=BD=E6=95=B0,=20=E9=9B=B6=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=8F=98=E6=9B=B4,=20ESLint=20errors=3D0/warnings=3D65/any=3D0?= =?UTF-8?q?,=20tsc=20-b=3D0,=20npm=20build=E2=9C=93,=2017/17=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_EVALUATION.md | 2 +- ROADMAP.md | 40 ++ TDD_PLAN.md | 51 ++ .../kubernetes/DeploymentEditAdvanced.tsx | 556 ++++-------------- .../HostSettingsCard.tsx | 54 ++ .../deployment-edit-advanced/PodMiscCard.tsx | 73 +++ .../PodSecurityCard.tsx | 57 ++ .../TolerationsCard.tsx | 61 ++ .../UpdateStrategyCard.tsx | 90 +++ .../deployment-edit-advanced/types.ts | 279 +++++++++ 10 files changed, 833 insertions(+), 430 deletions(-) create mode 100644 frontend/src/components/kubernetes/deployment-edit-advanced/HostSettingsCard.tsx create mode 100644 frontend/src/components/kubernetes/deployment-edit-advanced/PodMiscCard.tsx create mode 100644 frontend/src/components/kubernetes/deployment-edit-advanced/PodSecurityCard.tsx create mode 100644 frontend/src/components/kubernetes/deployment-edit-advanced/TolerationsCard.tsx create mode 100644 frontend/src/components/kubernetes/deployment-edit-advanced/UpdateStrategyCard.tsx create mode 100644 frontend/src/components/kubernetes/deployment-edit-advanced/types.ts diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 484f30c..36b66a3 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -129,7 +129,7 @@ | ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done | | 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) | | ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | -| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q20 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)**,累计提取 67 个独立组件文件** | +| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 → **Q6-Q21 done: 最大组件 ResourceVisualEditor 882L→282L (-68%) + AgentCLI 814L→521L (-36%) + SystemSettingsTab 754L→173L (-77%) + NamespaceResourcePage 719L→300L (-58%) + ApprovalWorkflowPage 705L→167L (-76%) + PipelineRunDetail 678L→273L (-60%) + CloudServerImport 699L→404L (-42%,any[]→具体接口) + UnifiedQueryPanel 682L→529L (-22%) + ServerManager 652L→442L (-32%) + PipelineEditor 676L→406L (-40%) + RdpDialog 642L→442L (-31%) + DeploymentEditService 627L→394L (-37%) + TraceManager 605L→331L (-45%,修复 SpanTreeNode 内联定义反模式) + GrafanaDashboard 624L→78L (-87%,DRY 合并 Zabbix/Prometheus Tab + 移除文件级 eslint-disable + 类型安全升级) + **TopologyView 596L→472L (-21%,提取 TopologyCanvas/NodeDetailDialog/EditNodeDialog,移除文件级 eslint-disable + any→ClusterInfo 具体接口)** + **DeploymentEditAdvanced 590L→289L (-51%,提取 types(parser+builder 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个 Card 组件)**,累计提取 73 个独立组件文件** | ### 3.2 🟡 中优先级 diff --git a/ROADMAP.md b/ROADMAP.md index ea9b321..8fd80c2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -492,6 +492,46 @@ services/api.ts (1324L) | Q18 | 前端巨型组件拆分(TraceManager 605L→331L,提取 types + trace-helpers + SpanTreeNode + TraceDetailDialog + SpanDetailDialog,修复 SpanTreeNode 内联定义反模式) | P0 | **done** | | Q19 | 前端巨型组件拆分(GrafanaDashboard 624L→78L,提取 types + datasource-toolbar + BatchDeployDialog + AgentTab + ExternalDatasourceTab,DRY 合并 Zabbix/Prometheus Tab,移除文件级 eslint-disable,类型安全升级 any→具体接口) | P0 | **done** | | Q20 | 前端巨型组件拆分(TopologyView 596L→472L,提取 types + TopologyCanvas + NodeDetailDialog + EditNodeDialog,移除文件级 eslint-disable,any→具体接口类型) | P0 | **done** | +| Q21 | 前端巨型组件拆分(DeploymentEditAdvanced 590L→289L,提取 types(parser+builder 纯函数) + UpdateStrategyCard + TolerationsCard + PodMiscCard + PodSecurityCard + HostSettingsCard) | P0 | **done** | + +--- + +## Q21: 前端巨型组件拆分 — DeploymentEditAdvanced [done] + +**问题**: `DeploymentEditAdvanced.tsx`(590 行)是 Q20 拆分后项目最大的 K8s 业务组件之一(排除 UI 库 sidebar/data-table),包含 Deployment 编辑器「高级设置」Tab 的全部逻辑:~30 个 useState、大段 useEffect manifest 解析(106 行)、大段 save 序列化函数(121 行)、以及 5 个内联 Card JSX 区块(更新策略/历史版本、容忍 Tolerations、Pod 级别杂项、Pod 安全上下文、主机命名空间设置)。单文件过大导致维护困难、代码导航低效。 + +**已完成 (2026-07-08)**: + +将类型/parser/builder 纯函数和 5 个内联 Card 提取为 `deployment-edit-advanced/` 子目录下的独立文件(该目录此前已有 SchedulingSection + DnsHostSection 两个组件): + +| 新文件 | 行数 | 职责 | +|--------|------|------| +| `types.ts` | 279 | Toleration/HostAlias/DnsOption/NodeInfo/AdvancedState 接口 + `parseAdvancedManifest()`(manifest→state 纯函数)+ `buildAdvancedManifest()`(state→manifest 纯函数) | +| `UpdateStrategyCard.tsx` | 90 | 更新策略 + 历史版本/进度卡片(RollingUpdate/Recreate 单选 + maxSurge/maxUnavailable + revisionHistoryLimit/progressDeadlineSeconds),受控组件接收 9 个 props | +| `TolerationsCard.tsx` | 61 | 容忍 (Tolerations) CRUD 卡片(key/operator/value/effect/tolerationSeconds 表格行 + 增删改),受控组件接收 4 个 props | +| `PodMiscCard.tsx` | 73 | Pod 级别杂项卡片(dnsPolicy/restartPolicy/schedulerName/terminationGracePeriodSeconds/serviceAccountName),受控组件接收 10 个 props | +| `PodSecurityCard.tsx` | 57 | Pod 安全上下文卡片(fsGroup/runAsUser/runAsGroup/runAsNonRoot),受控组件接收 8 个 props | +| `HostSettingsCard.tsx` | 54 | 主机命名空间设置卡片(hostNetwork/hostIPC/hostPID/shareProcessNamespace),受控组件接收 8 个 props | + +主文件 `DeploymentEditAdvanced.tsx` 从 **590 行降至 289 行**(-51%),仅保留:~30 个 useState 声明、useEffect(调用 `parseAdvancedManifest()` 解析 manifest + 加载集群节点列表 API)、labelSelector/toleration CRUD 辅助函数、save 函数(构建 AdvancedState 对象 → 调用 `buildAdvancedManifest()` → API 提交 + toast)、loading/empty 状态、以及 8 个子组件编排(UpdateStrategyCard + TolerationsCard + SchedulingSection + PodMiscCard + PodSecurityCard + HostSettingsCard + DnsHostSection + 操作栏)。 + +**关键改进**: +- **纯函数提取**: 原 useEffect 中的 106 行 manifest 解析逻辑和 save 函数中的 121 行 manifest 序列化逻辑,提取为 `parseAdvancedManifest(manifest): AdvancedState` 和 `buildAdvancedManifest(rawManifest, state): object` 两个纯函数。主组件的 useEffect 从 ~106 行降至 ~40 行(调用纯函数 + 逐字段 setState + 节点加载副作用),save 函数从 ~121 行降至 ~15 行(构建 state 对象 + 调用纯函数 + API 提交)。 +- **类型安全**: Toleration/HostAlias/DnsOption/NodeInfo 接口替代了之前分散的内联类型,AdvancedState 聚合接口使 parser/builder 函数签名清晰。catch 块从 `e?.message` 改为 `e instanceof Error ? e.message : String(e)` 类型安全访问。 +- **SchedulingSection 类型复用**: types.ts 从 SchedulingSection re-export `ScheduleMode`/`LabelSelectorEntry`/`ClusterNodeOption`,保持单一来源。 + +**验收标准**: +- [x] DeploymentEditAdvanced.tsx 从 590 行降至 **289 行**(-51%) +- [x] 6 个提取的文件全部 < 280 行(最大 types.ts 279 行含纯函数,Card 组件全部 < 91 行) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 17.36s) +- [x] ESLint errors 保持 **0**(DeploymentEditAdvanced + deployment-edit-advanced/ 目录零 errors/warnings) +- [x] ESLint warnings 保持 **65**(无新增) +- [x] 全局 ESLint any 保持 **0** +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) +- [x] Deployment 高级设置 Tab 功能不变(更新策略/历史版本/容忍/调度/Pod 杂项/安全上下文/主机设置/DNS+hostAliases 全部正常) --- diff --git a/TDD_PLAN.md b/TDD_PLAN.md index 9ae64c3..140e765 100644 --- a/TDD_PLAN.md +++ b/TDD_PLAN.md @@ -1380,6 +1380,57 @@ python3 tests/integration_test.py --host 127.0.0.1 --port 8080 - [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) - [x] 服务器管理页功能不变(列表/搜索/筛选/分页/选择/批量删除/CSV 导出/创建编辑/终端/RDP/文件浏览全部正常) +## Q21: 前端巨型组件拆分 — DeploymentEditAdvanced + +### 验证用例 + +```bash +# U1: Go 编译 +cd /srv/aiops/backend && go build ./... +# 期望: 零错误 + +# U2: TypeScript 编译 +cd /srv/aiops/frontend && npx tsc -b +# 期望: 零错误 + +# U3: Vite 构建 +npm run build 2>&1 | tail -3 +# 期望: built in Ns + +# U4: 拆分后文件行数 +wc -l src/components/kubernetes/DeploymentEditAdvanced.tsx src/components/kubernetes/deployment-edit-advanced/types.ts src/components/kubernetes/deployment-edit-advanced/UpdateStrategyCard.tsx src/components/kubernetes/deployment-edit-advanced/TolerationsCard.tsx src/components/kubernetes/deployment-edit-advanced/PodMiscCard.tsx src/components/kubernetes/deployment-edit-advanced/PodSecurityCard.tsx src/components/kubernetes/deployment-edit-advanced/HostSettingsCard.tsx +# 期望: DeploymentEditAdvanced < 300L, Card 组件 < 100L, types < 280L + +# U5: ESLint 全局指标 +npx eslint src/ --format json | python3 -c " +import json, sys +d = json.load(sys.stdin) +print(f'errors={sum(1 for r in d for m in r[\"messages\"] if m[\"severity\"]==2)}') +print(f'any={sum(1 for r in d for m in r[\"messages\"] if m.get(\"ruleId\")==\"@typescript-eslint/no-explicit-any\")}') +" +# 期望: errors=0, any=0 + +# U6: 集成测试 +python3 tests/integration_test.py --host 127.0.0.1 --port 8080 +# 期望: 17/17 passed +``` + +### 通过标准 +- [x] DeploymentEditAdvanced.tsx 从 590 行降至 **289 行**(< 300 ✅,-51%) +- [x] 6 个提取文件全部 < 280 行(types.ts 279L 含纯函数,5 个 Card 组件全部 < 91 行) +- [x] `tsc -b` 零错误 +- [x] `npm run build` 零错误(✓ built in 17.36s) +- [x] ESLint errors 保持 **0**(DeploymentEditAdvanced + deployment-edit-advanced/ 目录零 errors/warnings) +- [x] ESLint warnings 保持 **65**(无新增) +- [x] 全局 ESLint any 保持 **0** +- [x] `go build` 零错误 +- [x] 17/17 集成回归测试通过(零回归) +- [x] Docker 部署 healthy,前端页面正常加载(HTTP 200) +- [x] Deployment 高级设置 Tab 功能不变(更新策略/历史版本/容忍/调度/Pod 杂项/安全上下文/主机设置/DNS+hostAliases 全部正常) +- [x] manifest 解析和序列化提取为纯函数(parseAdvancedManifest + buildAdvancedManifest) + +--- + ## Q18: 前端巨型组件拆分 — TraceManager ### 验证用例 diff --git a/frontend/src/components/kubernetes/DeploymentEditAdvanced.tsx b/frontend/src/components/kubernetes/DeploymentEditAdvanced.tsx index 14d9b08..554d9aa 100644 --- a/frontend/src/components/kubernetes/DeploymentEditAdvanced.tsx +++ b/frontend/src/components/kubernetes/DeploymentEditAdvanced.tsx @@ -1,19 +1,24 @@ // DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8) // Logan.Li // 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / DNS / 安全上下文 / 终止宽限期 / 调度器 / SA +// Q21 拆分: 提取 types(parser+builder) + UpdateStrategyCard + TolerationsCard + PodMiscCard + PodSecurityCard + HostSettingsCard // 作者: 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 { Loader2, Save } 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 { SchedulingSection } from './deployment-edit-advanced/SchedulingSection'; import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection'; +import { + parseAdvancedManifest, buildAdvancedManifest, + type AdvancedState, type Toleration, type NodeInfo, +} from './deployment-edit-advanced/types'; +import { UpdateStrategyCard } from './deployment-edit-advanced/UpdateStrategyCard'; +import { TolerationsCard } from './deployment-edit-advanced/TolerationsCard'; +import { PodMiscCard } from './deployment-edit-advanced/PodMiscCard'; +import { PodSecurityCard } from './deployment-edit-advanced/PodSecurityCard'; +import { HostSettingsCard } from './deployment-edit-advanced/HostSettingsCard'; import type { JsonData } from '@/types/common'; import i18n from '@/i18n'; @@ -26,14 +31,6 @@ interface Props { 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); @@ -50,13 +47,15 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName, const [progressDeadlineSeconds, setProgressDeadlineSeconds] = useState('600'); // ──── 调度策略 ──── - const [scheduleMode, setScheduleMode] = useState('nodeSelector'); + const [scheduleMode, setScheduleMode] = useState('nodeSelector'); const [nodeName, setNodeName] = useState(''); - const [availableNodes, setAvailableNodes] = useState }>>([]); + const [availableNodes, setAvailableNodes] = useState([]); const [nodesLoading, setNodesLoading] = useState(false); const [labelSelectorKv, setLabelSelectorKv] = useState>([]); const [nodeLabels, setNodeLabels] = useState>({}); const [tolerations, setTolerations] = useState([]); + + // Pod 级别杂项 const [dnsPolicy, setDnsPolicy] = useState('ClusterFirst'); const [restartPolicy, setRestartPolicy] = useState('Always'); const [schedulerName, setSchedulerName] = useState('default-scheduler'); @@ -69,61 +68,57 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName, 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 补全) + // hostAliases const [hostAliases, setHostAliases] = useState>([]); - // dnsConfig(M8 补全) + // dnsConfig 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 || {}; + if (!manifest) { setLoading(false); return; } + setRawManifest(manifest); + const s = parseAdvancedManifest(manifest); + setStrategyType(s.strategyType); + setMaxSurge(s.maxSurge); + setMaxUnavailable(s.maxUnavailable); + setRevisionHistoryLimit(s.revisionHistoryLimit); + setProgressDeadlineSeconds(s.progressDeadlineSeconds); + setScheduleMode(s.scheduleMode); + setNodeName(s.nodeName); + setLabelSelectorKv(s.labelSelectorKv); + setTolerations(s.tolerations); + setDnsPolicy(s.dnsPolicy); + setRestartPolicy(s.restartPolicy); + setSchedulerName(s.schedulerName); + setTerminationGracePeriodSeconds(s.terminationGracePeriodSeconds); + setServiceAccountName(s.serviceAccountName); + setPodFsGroup(s.podFsGroup); + setPodRunAsUser(s.podRunAsUser); + setPodRunAsGroup(s.podRunAsGroup); + setPodRunAsNonRoot(s.podRunAsNonRoot); + setHostNetwork(s.hostNetwork); + setHostIPC(s.hostIPC); + setHostPID(s.hostPID); + setShareProcessNamespace(s.shareProcessNamespace); + setHostAliases(s.hostAliases); + setDnsNameservers(s.dnsNameservers); + setDnsSearches(s.dnsSearches); + setDnsOptions(s.dnsOptions); - // 更新策略 - 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'); - } - - // 加载节点列表 + // 加载节点列表(副作用,留在 useEffect 内) 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 mapped: NodeInfo[] = nodes.map((n: JsonData) => { const meta = n.metadata || {}; const status = n.status || {}; const addresses: JsonData[] = Array.isArray(status.addresses) ? status.addresses : []; @@ -143,50 +138,6 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName, 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); // eslint-disable-next-line react-hooks/exhaustive-deps }, [manifest]); @@ -197,252 +148,73 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName, const updateNs = (i: number, patch: Partial<{ key: string; value: string }>) => setLabelSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n))); + // tolerations 操作 + const addToleration = () => + setTolerations(prev => [...prev, { key: '', operator: 'Equal', value: '', effect: 'NoSchedule', tolerationSeconds: '' }]); + const removeToleration = (i: number) => setTolerations(prev => prev.filter((_, idx) => idx !== i)); + const updateToleration = (i: number, patch: Partial) => + setTolerations(prev => prev.map((t, idx) => (idx === i ? { ...t, ...patch } : t))); + 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; - + const state: AdvancedState = { + strategyType, maxSurge, maxUnavailable, + revisionHistoryLimit, progressDeadlineSeconds, + scheduleMode, nodeName, labelSelectorKv, tolerations, + dnsPolicy, restartPolicy, schedulerName, + terminationGracePeriodSeconds, serviceAccountName, + podFsGroup, podRunAsUser, podRunAsGroup, podRunAsNonRoot, + hostNetwork, hostIPC, hostPID, shareProcessNamespace, + hostAliases, dnsNameservers, dnsSearches, dnsOptions, + }; + const base = buildAdvancedManifest(rawManifest, state); await saveDeploymentManifest(clusterCode, namespace, deploymentName, base); toast({ title: i18n.t('kubernetes.deploymentedita.s9762d0a4'), description: `${deploymentName} 更新已提交` }); onSaved(base); } catch (e) { - toast({ title: i18n.t('kubernetes.msg.saveFailed'), description: e?.message || String(e), variant: 'destructive' }); + toast({ title: i18n.t('kubernetes.msg.saveFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' }); } finally { setSaving(false); } }; if (loading) { return ( - - - -

{i18n.t('monitor.logPanel.logResults.loading')}

-
-
+
+ +

{i18n.t('monitor.logPanel.logResults.loading')}

+
); } if (!manifest) { return ( - - - {i18n.t('k8s.editAdvanced.noManifest')} - - +
+ {i18n.t('k8s.editAdvanced.noManifest')} +
); } return (
- {/* 更新策略 */} - - - - {i18n.t('k8s.editAdvanced.updateStrategy')} - - - -
- - -
- {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 */} - - -
- {i18n.t('kubernetes.deploymentedita.s13a8310b')} - -
-
- - {tolerations.length === 0 &&

{i18n.t('kubernetes.deploymentedita.s8bc06bde')}

} - {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) - - - - - - - - - +
); -} \ No newline at end of file +} diff --git a/frontend/src/components/kubernetes/deployment-edit-advanced/HostSettingsCard.tsx b/frontend/src/components/kubernetes/deployment-edit-advanced/HostSettingsCard.tsx new file mode 100644 index 0000000..ee5085a --- /dev/null +++ b/frontend/src/components/kubernetes/deployment-edit-advanced/HostSettingsCard.tsx @@ -0,0 +1,54 @@ +// HostSettingsCard.tsx — 主机设置 (Host Namespace) 卡片 (Q21 提取) +// hostNetwork / hostIPC / hostPID / shareProcessNamespace +import { Network } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +interface HostSettingsCardProps { + hostNetwork: boolean; + onHostNetworkChange: (v: boolean) => void; + hostIPC: boolean; + onHostIPCChange: (v: boolean) => void; + hostPID: boolean; + onHostPIDChange: (v: boolean) => void; + shareProcessNamespace: boolean; + onShareProcessNamespaceChange: (v: boolean) => void; +} + +export function HostSettingsCard({ + hostNetwork, onHostNetworkChange, + hostIPC, onHostIPCChange, + hostPID, onHostPIDChange, + shareProcessNamespace, onShareProcessNamespaceChange, +}: HostSettingsCardProps) { + return ( + + + + 主机设置 (Host Namespace) + + + + + + + + + + ); +} diff --git a/frontend/src/components/kubernetes/deployment-edit-advanced/PodMiscCard.tsx b/frontend/src/components/kubernetes/deployment-edit-advanced/PodMiscCard.tsx new file mode 100644 index 0000000..a5d34eb --- /dev/null +++ b/frontend/src/components/kubernetes/deployment-edit-advanced/PodMiscCard.tsx @@ -0,0 +1,73 @@ +// PodMiscCard.tsx — Pod 级别杂项设置卡片 (Q21 提取) +// dnsPolicy / restartPolicy / schedulerName / terminationGracePeriodSeconds / serviceAccountName +import { Settings } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +interface PodMiscCardProps { + dnsPolicy: string; + onDnsPolicyChange: (v: string) => void; + restartPolicy: string; + onRestartPolicyChange: (v: string) => void; + schedulerName: string; + onSchedulerNameChange: (v: string) => void; + terminationGracePeriodSeconds: string; + onTerminationGracePeriodSecondsChange: (v: string) => void; + serviceAccountName: string; + onServiceAccountNameChange: (v: string) => void; +} + +export function PodMiscCard({ + dnsPolicy, onDnsPolicyChange, + restartPolicy, onRestartPolicyChange, + schedulerName, onSchedulerNameChange, + terminationGracePeriodSeconds, onTerminationGracePeriodSecondsChange, + serviceAccountName, onServiceAccountNameChange, +}: PodMiscCardProps) { + return ( + + + + Pod 级别 + + + +
+ + +
+
+ + +
+
+ + onSchedulerNameChange(e.target.value)} + className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ + onTerminationGracePeriodSecondsChange(e.target.value)} + className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ + onServiceAccountNameChange(e.target.value)} + placeholder="default" className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/deployment-edit-advanced/PodSecurityCard.tsx b/frontend/src/components/kubernetes/deployment-edit-advanced/PodSecurityCard.tsx new file mode 100644 index 0000000..63b75ca --- /dev/null +++ b/frontend/src/components/kubernetes/deployment-edit-advanced/PodSecurityCard.tsx @@ -0,0 +1,57 @@ +// PodSecurityCard.tsx — Pod 安全上下文 (securityContext) 卡片 (Q21 提取) +import { Shield } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +interface PodSecurityCardProps { + podFsGroup: string; + onPodFsGroupChange: (v: string) => void; + podRunAsUser: string; + onPodRunAsUserChange: (v: string) => void; + podRunAsGroup: string; + onPodRunAsGroupChange: (v: string) => void; + podRunAsNonRoot: boolean; + onPodRunAsNonRootChange: (v: boolean) => void; +} + +export function PodSecurityCard({ + podFsGroup, onPodFsGroupChange, + podRunAsUser, onPodRunAsUserChange, + podRunAsGroup, onPodRunAsGroupChange, + podRunAsNonRoot, onPodRunAsNonRootChange, +}: PodSecurityCardProps) { + return ( + + + + Pod 安全上下文 (securityContext) + + + +
+ + onPodFsGroupChange(e.target.value)} + placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ + onPodRunAsUserChange(e.target.value)} + placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ + onPodRunAsGroupChange(e.target.value)} + placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/kubernetes/deployment-edit-advanced/TolerationsCard.tsx b/frontend/src/components/kubernetes/deployment-edit-advanced/TolerationsCard.tsx new file mode 100644 index 0000000..2a7b2df --- /dev/null +++ b/frontend/src/components/kubernetes/deployment-edit-advanced/TolerationsCard.tsx @@ -0,0 +1,61 @@ +// TolerationsCard.tsx — 容忍 (Tolerations) CRUD 卡片 (Q21 提取) +// 受控展示组件,接收 tolerations 数组 + 增删改回调。 +import { Plus, Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import i18n from '@/i18n'; +import type { Toleration } from './types'; + +interface TolerationsCardProps { + tolerations: Toleration[]; + onAdd: () => void; + onRemove: (index: number) => void; + onUpdate: (index: number, patch: Partial) => void; +} + +export function TolerationsCard({ tolerations, onAdd, onRemove, onUpdate }: TolerationsCardProps) { + return ( + + +
+ {i18n.t('kubernetes.deploymentedita.s13a8310b')} + +
+
+ + {tolerations.length === 0 &&

{i18n.t('kubernetes.deploymentedita.s8bc06bde')}

} + {tolerations.map((t, i) => ( +
+ onUpdate(i, { key: e.target.value })} + className="h-7 dg-input text-xs font-mono" /> + + onUpdate(i, { value: e.target.value })} + className="h-7 dg-input text-xs font-mono" /> + + onUpdate(i, { tolerationSeconds: e.target.value })} + className="h-7 dg-input text-xs font-mono" /> + +
+ ))} +
+
+ ); +} diff --git a/frontend/src/components/kubernetes/deployment-edit-advanced/UpdateStrategyCard.tsx b/frontend/src/components/kubernetes/deployment-edit-advanced/UpdateStrategyCard.tsx new file mode 100644 index 0000000..02bfb13 --- /dev/null +++ b/frontend/src/components/kubernetes/deployment-edit-advanced/UpdateStrategyCard.tsx @@ -0,0 +1,90 @@ +// UpdateStrategyCard.tsx — 更新策略 + 历史版本/进度 卡片 (Q21 提取) +// 受控展示组件,接收 state 片段 + setter 回调。 +import { RefreshCw, Clock } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import i18n from '@/i18n'; + +interface UpdateStrategyCardProps { + strategyType: 'RollingUpdate' | 'Recreate'; + onStrategyTypeChange: (t: 'RollingUpdate' | 'Recreate') => void; + maxSurge: string; + onMaxSurgeChange: (v: string) => void; + maxUnavailable: string; + onMaxUnavailableChange: (v: string) => void; + revisionHistoryLimit: string; + onRevisionHistoryLimitChange: (v: string) => void; + progressDeadlineSeconds: string; + onProgressDeadlineSecondsChange: (v: string) => void; +} + +export function UpdateStrategyCard({ + strategyType, onStrategyTypeChange, + maxSurge, onMaxSurgeChange, + maxUnavailable, onMaxUnavailableChange, + revisionHistoryLimit, onRevisionHistoryLimitChange, + progressDeadlineSeconds, onProgressDeadlineSecondsChange, +}: UpdateStrategyCardProps) { + return ( + <> + {/* 更新策略 */} + + + + {i18n.t('k8s.editAdvanced.updateStrategy')} + + + +
+ + +
+ {strategyType === 'RollingUpdate' && ( +
+
+ + onMaxSurgeChange(e.target.value)} + placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ + onMaxUnavailableChange(e.target.value)} + placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ )} +
+
+ + {/* 历史版本 / 进度 */} + + + + 历史版本 & 进度 + + + +
+ + onRevisionHistoryLimitChange(e.target.value)} + className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+ + onProgressDeadlineSecondsChange(e.target.value)} + className="h-8 mt-1 dg-input text-sm font-mono" /> +
+
+
+ + ); +} diff --git a/frontend/src/components/kubernetes/deployment-edit-advanced/types.ts b/frontend/src/components/kubernetes/deployment-edit-advanced/types.ts new file mode 100644 index 0000000..8aad092 --- /dev/null +++ b/frontend/src/components/kubernetes/deployment-edit-advanced/types.ts @@ -0,0 +1,279 @@ +// types.ts — DeploymentEditAdvanced 共享类型 + 状态聚合接口 +// 提取自 DeploymentEditAdvanced.tsx (Q21 拆分),纯数据/类型定义,零 React 依赖。 +import type { JsonData } from '@/types/common'; + +// 从 SchedulingSection 复用调度相关类型,保持单一来源 +export type { ScheduleMode, LabelSelectorEntry, ClusterNodeOption } from './SchedulingSection'; + +export interface Toleration { + key: string; + operator: string; // Equal | Exists + value: string; + effect: string; // NoSchedule | PreferNoSchedule | NoExecute + tolerationSeconds: string; +} + +export interface HostAlias { + ip: string; + hostnames: string; +} + +export interface DnsOption { + name: string; + value: string; +} + +export interface NodeInfo { + name: string; + ip: string; + labels: Record; +} + +/** + * 聚合 DeploymentEditAdvanced 的全部高级设置状态。 + * 由 parseAdvancedManifest() 从 manifest 解析填充, + * 由 buildAdvancedManifest() 序列化回 manifest。 + */ +export interface AdvancedState { + // 更新策略 + strategyType: 'RollingUpdate' | 'Recreate'; + maxSurge: string; + maxUnavailable: string; + // 历史 / 进度 + revisionHistoryLimit: string; + progressDeadlineSeconds: string; + // 调度 + scheduleMode: 'nodeName' | 'nodeSelector' | 'nodeAffinity'; + nodeName: string; + labelSelectorKv: Array<{ key: string; value: string }>; + tolerations: Toleration[]; + // Pod 级别杂项 + dnsPolicy: string; + restartPolicy: string; + schedulerName: string; + terminationGracePeriodSeconds: string; + serviceAccountName: string; + // Pod 安全上下文 + podFsGroup: string; + podRunAsUser: string; + podRunAsGroup: string; + podRunAsNonRoot: boolean; + // 主机命名空间 + hostNetwork: boolean; + hostIPC: boolean; + hostPID: boolean; + shareProcessNamespace: boolean; + // hostAliases + hostAliases: HostAlias[]; + // dnsConfig + dnsNameservers: string; + dnsSearches: string; + dnsOptions: DnsOption[]; +} + +/** 从 manifest JSON 中提取 AdvancedState(纯函数,无副作用)。 */ +export function parseAdvancedManifest(m: JsonData | null): AdvancedState { + const empty: AdvancedState = { + strategyType: 'RollingUpdate', maxSurge: '25%', maxUnavailable: '25%', + revisionHistoryLimit: '10', progressDeadlineSeconds: '600', + scheduleMode: 'nodeSelector', nodeName: '', labelSelectorKv: [], tolerations: [], + dnsPolicy: 'ClusterFirst', restartPolicy: 'Always', schedulerName: 'default-scheduler', + terminationGracePeriodSeconds: '30', serviceAccountName: '', + podFsGroup: '', podRunAsUser: '', podRunAsGroup: '', podRunAsNonRoot: false, + hostNetwork: false, hostIPC: false, hostPID: false, shareProcessNamespace: false, + hostAliases: [], dnsNameservers: '', dnsSearches: '', dnsOptions: [], + }; + if (!m) return empty; + + const spec: JsonData = (m as JsonData).spec || {}; + const podSpec: JsonData = spec?.template?.spec || {}; + + // 更新策略 + const strat: JsonData = spec.strategy || {}; + const ru: JsonData = strat.rollingUpdate || {}; + const strategyType = (strat.type === 'Recreate' ? 'Recreate' : 'RollingUpdate') as 'RollingUpdate' | 'Recreate'; + + // nodeSelector → labelSelectorKv + const ns: JsonData = podSpec.nodeSelector || {}; + const nsEntries = Object.entries(ns).map(([k, v]) => ({ key: k, value: String(v) })); + + // Determine scheduleMode + let scheduleMode: AdvancedState['scheduleMode'] = 'nodeSelector'; + if (podSpec.nodeName) { + scheduleMode = 'nodeName'; + } else if (podSpec.affinity?.nodeAffinity) { + scheduleMode = 'nodeAffinity'; + } else if (nsEntries.length > 0) { + scheduleMode = 'nodeSelector'; + } + + // tolerations + const tols: JsonData[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : []; + const tolerations: Toleration[] = tols.map((t: JsonData) => ({ + key: t.key || '', + operator: t.operator || 'Equal', + value: t.value || '', + effect: t.effect || '', + tolerationSeconds: t.tolerationSeconds != null ? String(t.tolerationSeconds) : '', + })); + + // Pod securityContext + const sc: JsonData = podSpec.securityContext || {}; + + // hostAliases + const ha: JsonData[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : []; + const hostAliases: HostAlias[] = ha.map((a: JsonData) => ({ + ip: a.ip || '', + hostnames: Array.isArray(a.hostnames) ? a.hostnames.join(', ') : '', + })); + + // dnsConfig + const dc: JsonData = podSpec.dnsConfig || {}; + const opts: JsonData[] = Array.isArray(dc.options) ? dc.options : []; + + return { + strategyType, + maxSurge: String(ru.maxSurge ?? '25%'), + maxUnavailable: String(ru.maxUnavailable ?? '25%'), + revisionHistoryLimit: spec.revisionHistoryLimit != null ? String(spec.revisionHistoryLimit) : '10', + progressDeadlineSeconds: spec.progressDeadlineSeconds != null ? String(spec.progressDeadlineSeconds) : '600', + scheduleMode, + nodeName: podSpec.nodeName || '', + labelSelectorKv: nsEntries, + tolerations, + dnsPolicy: podSpec.dnsPolicy || 'ClusterFirst', + restartPolicy: podSpec.restartPolicy || 'Always', + schedulerName: podSpec.schedulerName || 'default-scheduler', + terminationGracePeriodSeconds: podSpec.terminationGracePeriodSeconds != null ? String(podSpec.terminationGracePeriodSeconds) : '30', + serviceAccountName: podSpec.serviceAccountName || '', + podFsGroup: sc.fsGroup != null ? String(sc.fsGroup) : '', + podRunAsUser: sc.runAsUser != null ? String(sc.runAsUser) : '', + podRunAsGroup: sc.runAsGroup != null ? String(sc.runAsGroup) : '', + podRunAsNonRoot: !!sc.runAsNonRoot, + hostNetwork: !!podSpec.hostNetwork, + hostIPC: !!podSpec.hostIPC, + hostPID: !!podSpec.hostPID, + shareProcessNamespace: !!podSpec.shareProcessNamespace, + hostAliases, + dnsNameservers: Array.isArray(dc.nameservers) ? dc.nameservers.join('\n') : '', + dnsSearches: Array.isArray(dc.searches) ? dc.searches.join('\n') : '', + dnsOptions: opts.map((o: JsonData) => ({ name: o.name || '', value: o.value || '' })), + }; +} + +/** 将 AdvancedState 序列化回 manifest JSON(纯函数,返回深拷贝后的新 manifest)。 */ +export function buildAdvancedManifest(rawManifest: JsonData, s: AdvancedState): JsonData { + 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: JsonData = base.spec.template.spec; + + // 更新策略 + base.spec.strategy = { type: s.strategyType }; + if (s.strategyType === 'RollingUpdate') { + base.spec.strategy.rollingUpdate = { maxSurge: s.maxSurge, maxUnavailable: s.maxUnavailable }; + } + + // 历史 / 进度 + if (s.revisionHistoryLimit) base.spec.revisionHistoryLimit = Number(s.revisionHistoryLimit); + if (s.progressDeadlineSeconds) base.spec.progressDeadlineSeconds = Number(s.progressDeadlineSeconds); + + // nodeSelector / nodeName / nodeAffinity + if (s.scheduleMode === 'nodeName' && s.nodeName.trim()) { + podSpec.nodeName = s.nodeName.trim(); + delete podSpec.nodeSelector; + if (podSpec.affinity) delete podSpec.affinity.nodeAffinity; + } else if (s.scheduleMode === 'nodeSelector') { + const nsObj: Record = {}; + for (const { key, value } of s.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 (s.scheduleMode === 'nodeAffinity') { + const matchExpressions: JsonData[] = s.labelSelectorKv + .filter((item) => item.key.trim()) + .map((item) => ({ key: item.key.trim(), operator: 'In', values: [item.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 = s.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 (s.dnsPolicy) podSpec.dnsPolicy = s.dnsPolicy; + if (s.restartPolicy) podSpec.restartPolicy = s.restartPolicy; + if (s.schedulerName) podSpec.schedulerName = s.schedulerName; + if (s.terminationGracePeriodSeconds) podSpec.terminationGracePeriodSeconds = Number(s.terminationGracePeriodSeconds); + if (s.serviceAccountName) podSpec.serviceAccountName = s.serviceAccountName; + + // Pod 安全上下文 + const sc: JsonData = {}; + if (s.podFsGroup) sc.fsGroup = Number(s.podFsGroup); + if (s.podRunAsUser) sc.runAsUser = Number(s.podRunAsUser); + if (s.podRunAsGroup) sc.runAsGroup = Number(s.podRunAsGroup); + if (s.podRunAsNonRoot) sc.runAsNonRoot = true; + if (Object.keys(sc).length > 0) podSpec.securityContext = sc; + else delete podSpec.securityContext; + + // 主机设置 — 只在为 true 时写入,false 删除字段 + if (s.hostNetwork) podSpec.hostNetwork = true; else delete podSpec.hostNetwork; + if (s.hostIPC) podSpec.hostIPC = true; else delete podSpec.hostIPC; + if (s.hostPID) podSpec.hostPID = true; else delete podSpec.hostPID; + if (s.shareProcessNamespace) podSpec.shareProcessNamespace = true; else delete podSpec.shareProcessNamespace; + + // hostAliases + const haK8s = s.hostAliases + .filter((a) => a.ip.trim()) + .map((a) => ({ + ip: a.ip.trim(), + hostnames: a.hostnames.split(/[,\s]+/).map((str: string) => str.trim()).filter(Boolean), + })) + .filter((a) => a.hostnames.length > 0); + if (haK8s.length > 0) podSpec.hostAliases = haK8s; + else delete podSpec.hostAliases; + + // dnsConfig + const dcObj: JsonData = {}; + const nsArr = s.dnsNameservers.split('\n').map((str: string) => str.trim()).filter(Boolean); + if (nsArr.length > 0) dcObj.nameservers = nsArr; + const srArr = s.dnsSearches.split('\n').map((str: string) => str.trim()).filter(Boolean); + if (srArr.length > 0) dcObj.searches = srArr; + const dkOpts = s.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; + + return base; +}