refactor(quality): Q21 前端巨型组件拆分 — DeploymentEditAdvanced 590L→289L (-51%), 提取 types(parseAdvancedManifest+buildAdvancedManifest 纯函数) + UpdateStrategyCard/TolerationsCard/PodMiscCard/PodSecurityCard/HostSettingsCard 5 个受控 Card 组件, manifest 解析(106L)和序列化(121L)提取为纯函数, 零逻辑变更, ESLint errors=0/warnings=65/any=0, tsc -b=0, npm build✓, 17/17 测试通过

This commit is contained in:
attacker committed 2026-07-08 20:37:03 +08:00
1 parent a530d3898c
commit b4591de97c
10 files changed
+833 -430

No files matched your search

@@ -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 (
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Network className="h-4 w-4 dg-text-cyan" />主机设置 (Host Namespace)
</CardTitle>
</CardHeader>
<CardContent className="pt-0 grid grid-cols-2 gap-3">
<label className="flex items-center gap-2 text-xs text-[var(--dg-text-secondary)] cursor-pointer">
<input type="checkbox" checked={hostNetwork} onChange={e => onHostNetworkChange(e.target.checked)}
className="accent-blue-500" />
hostNetwork(共享主机网络命名空间)
</label>
<label className="flex items-center gap-2 text-xs text-[var(--dg-text-secondary)] cursor-pointer">
<input type="checkbox" checked={hostIPC} onChange={e => onHostIPCChange(e.target.checked)}
className="accent-blue-500" />
hostIPC(共享主机 IPC 命名空间)
</label>
<label className="flex items-center gap-2 text-xs text-[var(--dg-text-secondary)] cursor-pointer">
<input type="checkbox" checked={hostPID} onChange={e => onHostPIDChange(e.target.checked)}
className="accent-blue-500" />
hostPID(共享主机 PID 命名空间)
</label>
<label className="flex items-center gap-2 text-xs text-[var(--dg-text-secondary)] cursor-pointer">
<input type="checkbox" checked={shareProcessNamespace} onChange={e => onShareProcessNamespaceChange(e.target.checked)}
className="accent-blue-500" />
shareProcessNamespace(共享进程命名空间)
</label>
</CardContent>
</Card>
);
}
@@ -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 (
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Settings className="h-4 w-4 dg-text-secondary" />Pod 级别
</CardTitle>
</CardHeader>
<CardContent className="pt-0 grid grid-cols-2 gap-3">
<div>
<Label className="dg-text-muted text-xs">dnsPolicy</Label>
<select value={dnsPolicy} onChange={e => onDnsPolicyChange(e.target.value)}
className="h-8 mt-1 w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-2">
<option value="ClusterFirst">ClusterFirst</option>
<option value="ClusterFirstWithHostNet">ClusterFirstWithHostNet</option>
<option value="Default">Default</option>
<option value="None">None</option>
</select>
</div>
<div>
<Label className="dg-text-muted text-xs">restartPolicy</Label>
<select value={restartPolicy} onChange={e => onRestartPolicyChange(e.target.value)}
className="h-8 mt-1 w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-2">
<option value="Always">Always</option>
<option value="OnFailure">OnFailure</option>
<option value="Never">Never</option>
</select>
</div>
<div>
<Label className="dg-text-muted text-xs">schedulerName</Label>
<Input value={schedulerName} onChange={e => onSchedulerNameChange(e.target.value)}
className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div>
<Label className="dg-text-muted text-xs">terminationGracePeriodSeconds</Label>
<Input type="number" value={terminationGracePeriodSeconds} onChange={e => onTerminationGracePeriodSecondsChange(e.target.value)}
className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div className="col-span-2">
<Label className="dg-text-muted text-xs">serviceAccountName</Label>
<Input value={serviceAccountName} onChange={e => onServiceAccountNameChange(e.target.value)}
placeholder="default" className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
</CardContent>
</Card>
);
}
@@ -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 (
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Shield className="h-4 w-4 text-rose-400" />Pod 安全上下文 (securityContext)
</CardTitle>
</CardHeader>
<CardContent className="pt-0 grid grid-cols-2 gap-3">
<div>
<Label className="dg-text-muted text-xs">fsGroup</Label>
<Input type="number" value={podFsGroup} onChange={e => onPodFsGroupChange(e.target.value)}
placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div>
<Label className="dg-text-muted text-xs">runAsUser</Label>
<Input type="number" value={podRunAsUser} onChange={e => onPodRunAsUserChange(e.target.value)}
placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div>
<Label className="dg-text-muted text-xs">runAsGroup</Label>
<Input type="number" value={podRunAsGroup} onChange={e => onPodRunAsGroupChange(e.target.value)}
placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div className="flex items-end pb-1">
<label className="flex items-center gap-2 text-xs dg-text-secondary cursor-pointer">
<input type="checkbox" checked={podRunAsNonRoot} onChange={e => onPodRunAsNonRootChange(e.target.checked)}
className="accent-blue-500" />
runAsNonRoot
</label>
</div>
</CardContent>
</Card>
);
}
@@ -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<Toleration>) => void;
}
export function TolerationsCard({ tolerations, onAdd, onRemove, onUpdate }: TolerationsCardProps) {
return (
<Card className="dg-card">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('kubernetes.deploymentedita.s13a8310b')} </CardTitle>
<Button size="sm" variant="outline" className="h-7 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs"
onClick={onAdd}>
<Plus className="h-3 w-3 mr-1" />添加
</Button>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-2">
{tolerations.length === 0 && <p className="text-xs dg-text-muted"> {i18n.t('kubernetes.deploymentedita.s8bc06bde')} </p>}
{tolerations.map((t, i) => (
<div key={i} className="grid grid-cols-[1fr_80px_1fr_100px_80px_auto] gap-2 items-center">
<Input value={t.key} placeholder="key" onChange={e => onUpdate(i, { key: e.target.value })}
className="h-7 dg-input text-xs font-mono" />
<select value={t.operator} onChange={e => onUpdate(i, { operator: e.target.value })}
className="h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1">
<option value="Equal">Equal</option>
<option value="Exists">Exists</option>
</select>
<Input value={t.value} placeholder="value" disabled={t.operator === 'Exists'}
onChange={e => onUpdate(i, { value: e.target.value })}
className="h-7 dg-input text-xs font-mono" />
<select value={t.effect} onChange={e => onUpdate(i, { effect: e.target.value })}
className="h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1">
<option value=""> {i18n.t('kubernetes.deploymentedita.s2a6eb2dd')} </option>
<option value="NoSchedule">NoSchedule</option>
<option value="PreferNoSchedule">PreferNoSchedule</option>
<option value="NoExecute">NoExecute</option>
</select>
<Input value={t.tolerationSeconds} placeholder={i18n.t('k8s.editAdvanced.s0c1fec65')} disabled={t.effect !== 'NoExecute'}
onChange={e => onUpdate(i, { tolerationSeconds: e.target.value })}
className="h-7 dg-input text-xs font-mono" />
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-red" onClick={() => onRemove(i)}>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
</CardContent>
</Card>
);
}
@@ -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 (
<>
{/* 更新策略 */}
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<RefreshCw className="h-4 w-4 dg-text-blue" />{i18n.t('k8s.editAdvanced.updateStrategy')}
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3">
<div className="flex items-center gap-4">
<label className="flex items-center gap-1.5 text-xs text-[var(--dg-text-secondary)] cursor-pointer">
<input type="radio" checked={strategyType === 'RollingUpdate'} onChange={() => onStrategyTypeChange('RollingUpdate')}
className="accent-blue-500" />
RollingUpdate(滚动更新)
</label>
<label className="flex items-center gap-1.5 text-xs text-[var(--dg-text-secondary)] cursor-pointer">
<input type="radio" checked={strategyType === 'Recreate'} onChange={() => onStrategyTypeChange('Recreate')}
className="accent-blue-500" />
Recreate(重建)
</label>
</div>
{strategyType === 'RollingUpdate' && (
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="dg-text-muted text-xs">maxSurge(整数或百分比,超出期望副本数)</Label>
<Input value={maxSurge} onChange={e => onMaxSurgeChange(e.target.value)}
placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div>
<Label className="dg-text-muted text-xs">maxUnavailable(整数或百分比,不可用上限)</Label>
<Input value={maxUnavailable} onChange={e => onMaxUnavailableChange(e.target.value)}
placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
</div>
)}
</CardContent>
</Card>
{/* 历史版本 / 进度 */}
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Clock className="h-4 w-4 dg-text-yellow" />历史版本 &amp; 进度
</CardTitle>
</CardHeader>
<CardContent className="pt-0 grid grid-cols-2 gap-3">
<div>
<Label className="dg-text-muted text-xs">revisionHistoryLimit(保留 RS 历史)</Label>
<Input type="number" value={revisionHistoryLimit} onChange={e => onRevisionHistoryLimitChange(e.target.value)}
className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
<div>
<Label className="dg-text-muted text-xs">progressDeadlineSeconds(升级超时秒数)</Label>
<Input type="number" value={progressDeadlineSeconds} onChange={e => onProgressDeadlineSecondsChange(e.target.value)}
className="h-8 mt-1 dg-input text-sm font-mono" />
</div>
</CardContent>
</Card>
</>
);
}
@@ -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<string, string>;
}
/**
* 聚合 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<string, string> = {};
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;
}