290 lines
12 KiB
TypeScript
290 lines
12 KiB
TypeScript
// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8) // Logan.Li
|
|
// 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / DNS / 安全上下文 / 终止宽限期 / 调度器 / SA
|
|
// Q21 拆分: 提取 types(parser+builder) + UpdateStrategyCard + TolerationsCard + PodMiscCard + PodSecurityCard + HostSettingsCard
|
|
// 作者: Logan <admin@attacker.club>
|
|
import { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Loader2, Save } from 'lucide-react';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { kubernetesApi } from '@/api';
|
|
import { saveDeploymentManifest } from './deployment-manifest-save';
|
|
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';
|
|
|
|
interface Props {
|
|
clusterCode: string;
|
|
namespace: string;
|
|
deploymentName: string;
|
|
manifest: JsonData | null;
|
|
onSaved: (updated?: JsonData) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
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<JsonData>(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<AdvancedState['scheduleMode']>('nodeSelector');
|
|
const [nodeName, setNodeName] = useState('');
|
|
const [availableNodes, setAvailableNodes] = useState<NodeInfo[]>([]);
|
|
const [nodesLoading, setNodesLoading] = useState(false);
|
|
const [labelSelectorKv, setLabelSelectorKv] = useState<Array<{ key: string; value: string }>>([]);
|
|
const [nodeLabels, setNodeLabels] = useState<Record<string, string[]>>({});
|
|
const [tolerations, setTolerations] = useState<Toleration[]>([]);
|
|
|
|
// Pod 级别杂项
|
|
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);
|
|
|
|
// 主机设置
|
|
const [hostNetwork, setHostNetwork] = useState(false);
|
|
const [hostIPC, setHostIPC] = useState(false);
|
|
const [hostPID, setHostPID] = useState(false);
|
|
const [shareProcessNamespace, setShareProcessNamespace] = useState(false);
|
|
|
|
// hostAliases
|
|
const [hostAliases, setHostAliases] = useState<Array<{ ip: string; hostnames: string }>>([]);
|
|
|
|
// dnsConfig
|
|
const [dnsNameservers, setDnsNameservers] = useState('');
|
|
const [dnsSearches, setDnsSearches] = useState('');
|
|
const [dnsOptions, setDnsOptions] = useState<Array<{ name: string; value: string }>>([]);
|
|
|
|
useEffect(() => {
|
|
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);
|
|
|
|
// 加载节点列表(副作用,留在 useEffect 内)
|
|
if (clusterCode) {
|
|
setNodesLoading(true);
|
|
kubernetesApi.getClusterNodes(clusterCode).then((res: JsonData) => {
|
|
const nodes: JsonData[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
|
|
const mapped: NodeInfo[] = 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<string, string[]> = {};
|
|
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));
|
|
}
|
|
setLoading(false);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [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)));
|
|
|
|
// 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<Toleration>) =>
|
|
setTolerations(prev => prev.map((t, idx) => (idx === i ? { ...t, ...patch } : t)));
|
|
|
|
const save = async () => {
|
|
setSaving(true);
|
|
try {
|
|
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 instanceof Error ? e.message : String(e), variant: 'destructive' });
|
|
} finally { setSaving(false); }
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-12 text-center">
|
|
<Loader2 className="h-6 w-6 animate-spin mx-auto dg-text-muted" />
|
|
<p className="dg-text-muted mt-3 text-sm"> {i18n.t('monitor.logPanel.logResults.loading')} </p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!manifest) {
|
|
return (
|
|
<div className="p-8 text-center dg-text-muted">
|
|
{i18n.t('k8s.editAdvanced.noManifest')}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<UpdateStrategyCard
|
|
strategyType={strategyType}
|
|
onStrategyTypeChange={setStrategyType}
|
|
maxSurge={maxSurge}
|
|
onMaxSurgeChange={setMaxSurge}
|
|
maxUnavailable={maxUnavailable}
|
|
onMaxUnavailableChange={setMaxUnavailable}
|
|
revisionHistoryLimit={revisionHistoryLimit}
|
|
onRevisionHistoryLimitChange={setRevisionHistoryLimit}
|
|
progressDeadlineSeconds={progressDeadlineSeconds}
|
|
onProgressDeadlineSecondsChange={setProgressDeadlineSeconds}
|
|
/>
|
|
|
|
<TolerationsCard
|
|
tolerations={tolerations}
|
|
onAdd={addToleration}
|
|
onRemove={removeToleration}
|
|
onUpdate={updateToleration}
|
|
/>
|
|
|
|
<SchedulingSection
|
|
scheduleMode={scheduleMode}
|
|
onScheduleModeChange={setScheduleMode}
|
|
nodeName={nodeName}
|
|
onNodeNameChange={setNodeName}
|
|
availableNodes={availableNodes}
|
|
nodesLoading={nodesLoading}
|
|
labelSelectorKv={labelSelectorKv}
|
|
onAddLabelSelector={addNs}
|
|
onRemoveLabelSelector={removeNs}
|
|
onUpdateLabelSelector={updateNs}
|
|
nodeLabels={nodeLabels}
|
|
/>
|
|
|
|
<PodMiscCard
|
|
dnsPolicy={dnsPolicy}
|
|
onDnsPolicyChange={setDnsPolicy}
|
|
restartPolicy={restartPolicy}
|
|
onRestartPolicyChange={setRestartPolicy}
|
|
schedulerName={schedulerName}
|
|
onSchedulerNameChange={setSchedulerName}
|
|
terminationGracePeriodSeconds={terminationGracePeriodSeconds}
|
|
onTerminationGracePeriodSecondsChange={setTerminationGracePeriodSeconds}
|
|
serviceAccountName={serviceAccountName}
|
|
onServiceAccountNameChange={setServiceAccountName}
|
|
/>
|
|
|
|
<PodSecurityCard
|
|
podFsGroup={podFsGroup}
|
|
onPodFsGroupChange={setPodFsGroup}
|
|
podRunAsUser={podRunAsUser}
|
|
onPodRunAsUserChange={setPodRunAsUser}
|
|
podRunAsGroup={podRunAsGroup}
|
|
onPodRunAsGroupChange={setPodRunAsGroup}
|
|
podRunAsNonRoot={podRunAsNonRoot}
|
|
onPodRunAsNonRootChange={setPodRunAsNonRoot}
|
|
/>
|
|
|
|
<HostSettingsCard
|
|
hostNetwork={hostNetwork}
|
|
onHostNetworkChange={setHostNetwork}
|
|
hostIPC={hostIPC}
|
|
onHostIPCChange={setHostIPC}
|
|
hostPID={hostPID}
|
|
onHostPIDChange={setHostPID}
|
|
shareProcessNamespace={shareProcessNamespace}
|
|
onShareProcessNamespaceChange={setShareProcessNamespace}
|
|
/>
|
|
|
|
<DnsHostSection
|
|
dnsNameservers={dnsNameservers}
|
|
onDnsNameserversChange={setDnsNameservers}
|
|
dnsSearches={dnsSearches}
|
|
onDnsSearchesChange={setDnsSearches}
|
|
dnsOptions={dnsOptions}
|
|
onDnsOptionsChange={setDnsOptions}
|
|
hostAliases={hostAliases}
|
|
onHostAliasesChange={setHostAliases}
|
|
/>
|
|
|
|
{/* 操作栏 */}
|
|
<div className="flex justify-end gap-2 pt-2">
|
|
<Button variant="outline" className="border-[var(--dg-border)] dg-text-secondary" onClick={onCancel}> {i18n.t('rbac.jobRole.cancel')} </Button>
|
|
<Button className="bg-blue-600 hover:bg-blue-700" disabled={saving} onClick={save}>
|
|
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Save className="h-4 w-4 mr-1" />}
|
|
保存
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|