feat(alert-center): 添加飞书Webhook通知渠道 + 告警引擎实时通知
- 后端: NotifyChannel 模型新增 feishu 类型, FeishuWebhookConfig - 后端: TestNotifyChannel 真实发送飞书测试消息(文本格式) - 后端: 告警引擎触发/恢复时自动发送飞书通知(sendFeishuAlert/sendFeishuRecovery) - 后端: AlertEngine 新增 tcp_time_wait/tcp_close_wait/service_process 指标检测 - 后端: NetworkStatModel 新增 TCPCloseWait 字段 - 后端: QuickSetupRules 一键创建6条告警规则(CPU/内存/磁盘/TIME_WAIT/CLOSE_WAIT/进程) - 后端: 注册 /alerts/quick-setup 路由 - 前端: AlertChannelDialog 新增飞书类型选项 - 前端: helpers 新增 getChannelIcon/getChannelTypeLabel 支持 feishu/dingtalk/wechat
This commit is contained in:
1 parent
7bc5169e8f
commit
5333f6f04a
30 files changed
+2406
-1001
No files matched your search
@@ -45,8 +45,15 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
const [revisionHistoryLimit, setRevisionHistoryLimit] = useState('10');
|
||||
const [progressDeadlineSeconds, setProgressDeadlineSeconds] = useState('600');
|
||||
|
||||
// Pod 级别
|
||||
const [nodeSelectorKv, setNodeSelectorKv] = useState<Array<{ key: string; value: string }>>([]);
|
||||
// ──── 调度策略 ────
|
||||
const [scheduleMode, setScheduleMode] = useState<'nodeName' | 'nodeSelector' | 'nodeAffinity'>('nodeSelector');
|
||||
const [nodeName, setNodeName] = useState('');
|
||||
const [availableNodes, setAvailableNodes] = useState<Array<{ name: string; ip: string; labels: Record<string, string> }>>([]);
|
||||
const [nodesLoading, setNodesLoading] = useState(false);
|
||||
// 标签选择器 — key=value 列表(替代原 nodeSelectorKv)
|
||||
const [labelSelectorKv, setLabelSelectorKv] = useState<Array<{ key: string; value: string }>>([]);
|
||||
// 预置标签(从节点聚合)
|
||||
const [nodeLabels, setNodeLabels] = useState<Record<string, string[]>>({});
|
||||
const [tolerations, setTolerations] = useState<Toleration[]>([]);
|
||||
const [dnsPolicy, setDnsPolicy] = useState('ClusterFirst');
|
||||
const [restartPolicy, setRestartPolicy] = useState('Always');
|
||||
@@ -92,9 +99,48 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
setRevisionHistoryLimit(spec.revisionHistoryLimit != null ? String(spec.revisionHistoryLimit) : '10');
|
||||
setProgressDeadlineSeconds(spec.progressDeadlineSeconds != null ? String(spec.progressDeadlineSeconds) : '600');
|
||||
|
||||
// nodeSelector
|
||||
// nodeSelector → labelSelectorKv
|
||||
const ns = podSpec.nodeSelector || {};
|
||||
setNodeSelectorKv(Object.entries(ns).map(([k, v]) => ({ key: k, value: String(v) })));
|
||||
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: any) => {
|
||||
const nodes: any[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
|
||||
const mapped = nodes.map((n: any) => {
|
||||
const meta = n.metadata || {};
|
||||
const status = n.status || {};
|
||||
const addresses: any[] = Array.isArray(status.addresses) ? status.addresses : [];
|
||||
const internalIP = addresses.find((a: any) => 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));
|
||||
}
|
||||
|
||||
// tolerations
|
||||
const tols: any[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : [];
|
||||
@@ -142,11 +188,11 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
setLoading(false);
|
||||
}, [manifest]);
|
||||
|
||||
// nodeSelector 操作
|
||||
const addNs = () => setNodeSelectorKv(prev => [...prev, { key: '', value: '' }]);
|
||||
const removeNs = (i: number) => setNodeSelectorKv(prev => prev.filter((_, idx) => idx !== i));
|
||||
// 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 }>) =>
|
||||
setNodeSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n)));
|
||||
setLabelSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n)));
|
||||
|
||||
// tolerations 操作
|
||||
const addTol = () => setTolerations(prev => [...prev, { key: '', operator: 'Equal', value: '', effect: 'NoSchedule', tolerationSeconds: '' }]);
|
||||
@@ -173,14 +219,38 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
if (revisionHistoryLimit) base.spec.revisionHistoryLimit = Number(revisionHistoryLimit);
|
||||
if (progressDeadlineSeconds) base.spec.progressDeadlineSeconds = Number(progressDeadlineSeconds);
|
||||
|
||||
// nodeSelector
|
||||
const nsObj: Record<string, string> = {};
|
||||
for (const { key, value } of nodeSelectorKv) {
|
||||
const k = key.trim();
|
||||
if (k) nsObj[k] = value;
|
||||
// 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<string, string> = {};
|
||||
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: any[] = 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;
|
||||
}
|
||||
if (Object.keys(nsObj).length > 0) podSpec.nodeSelector = nsObj;
|
||||
else delete podSpec.nodeSelector;
|
||||
|
||||
// tolerations
|
||||
const tolsK8s = tolerations
|
||||
@@ -335,91 +405,135 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 调度:nodeSelector + tolerations */}
|
||||
{/* 调度:节点名称 / 标签选择器 / 节点亲和性 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />调度 (Scheduling)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
{/* nodeSelector */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">nodeSelector(节点标签选择)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addNs}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />调度 (Scheduling)
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1 bg-slate-800 rounded-lg p-0.5 border border-slate-700">
|
||||
{([
|
||||
{ key: 'nodeName' as const, label: '节点名称', desc: '指定节点' },
|
||||
{ key: 'nodeSelector' as const, label: '标签选择器', desc: 'key=value' },
|
||||
{ key: 'nodeAffinity' as const, label: '节点亲和性', desc: '高级规则' },
|
||||
]).map(m => (
|
||||
<button key={m.key} onClick={() => setScheduleMode(m.key)}
|
||||
className={`px-2.5 py-1 rounded text-[10px] font-medium transition-all ${
|
||||
scheduleMode === m.key
|
||||
? 'bg-emerald-500/20 text-emerald-300 shadow-sm'
|
||||
: 'text-slate-400 hover:text-slate-300'
|
||||
}`}>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{nodeSelectorKv.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : nodeSelectorKv.map((n, i) => (
|
||||
<div key={i} className="flex items-center gap-2 mb-1.5">
|
||||
<Input value={n.key} onChange={e => updateNs(i, { key: e.target.value })}
|
||||
placeholder="disktype" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<span className="text-slate-600">:</span>
|
||||
<Input value={n.value} onChange={e => updateNs(i, { value: e.target.value })}
|
||||
placeholder="ssd" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeNs(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{scheduleMode === 'nodeName' && 'Pod 将直接调度到指定节点'}
|
||||
{scheduleMode === 'nodeSelector' && 'Pod 将调度到匹配标签的节点'}
|
||||
{scheduleMode === 'nodeAffinity' && '通过亲和性规则控制 Pod 调度到指定节点'}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{/* 节点名称模式 */}
|
||||
{scheduleMode === 'nodeName' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-400 text-xs">请选择节点</Label>
|
||||
{nodesLoading ? (
|
||||
<div className="flex items-center gap-1 text-slate-500 text-xs"><Loader2 className="h-3 w-3 animate-spin" />加载节点列表...</div>
|
||||
) : (
|
||||
<select value={nodeName} onChange={e => setNodeName(e.target.value)}
|
||||
className="h-8 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2">
|
||||
<option value="">(不指定)</option>
|
||||
{availableNodes.map(n => (
|
||||
<option key={n.name} value={n.name}>{n.name} {n.ip ? `(${n.ip})` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{nodeName && (
|
||||
<div className="text-[10px] text-slate-500">
|
||||
已选择: <code className="bg-slate-800 px-1 py-0.5 rounded text-emerald-400">{nodeName}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签选择器模式 */}
|
||||
{scheduleMode === 'nodeSelector' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-slate-400 text-xs">请选择标签</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addNs}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* tolerations */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">tolerations(污点容忍)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addTol}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{tolerations.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : tolerations.map((t, i) => (
|
||||
<div key={i} className="bg-slate-950/50 border border-slate-800 rounded p-2 mb-1.5 grid grid-cols-5 gap-2 items-end">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">key</Label>
|
||||
<Input value={t.key} onChange={e => updateTol(i, { key: e.target.value })}
|
||||
placeholder="node-role.kubernetes.io/control-plane"
|
||||
className="h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">operator</Label>
|
||||
<select value={t.operator} onChange={e => updateTol(i, { operator: e.target.value })}
|
||||
className="h-7 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1">
|
||||
<option value="Equal">Equal</option>
|
||||
<option value="Exists">Exists</option>
|
||||
{labelSelectorKv.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置标签选择器</p>
|
||||
) : labelSelectorKv.map((n, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
{/* 标签 Key 下拉 */}
|
||||
<select value={n.key} onChange={e => updateNs(i, { key: e.target.value, value: '' })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||
<option value="">选择标签</option>
|
||||
{Object.entries(nodeLabels).map(([k, vals]) => (
|
||||
<option key={k} value={k}>{k} ({vals.length})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">value</Label>
|
||||
<Input value={t.value} onChange={e => updateTol(i, { value: e.target.value })}
|
||||
disabled={t.operator === 'Exists'}
|
||||
className="h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono disabled:opacity-40" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">effect</Label>
|
||||
<select value={t.effect} onChange={e => updateTol(i, { effect: e.target.value })}
|
||||
className="h-7 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1">
|
||||
<option value="">(all)</option>
|
||||
<option value="NoSchedule">NoSchedule</option>
|
||||
<option value="PreferNoSchedule">PreferNoSchedule</option>
|
||||
<option value="NoExecute">NoExecute</option>
|
||||
{/* 标签 Value 下拉 */}
|
||||
<select value={n.value} onChange={e => updateNs(i, { value: e.target.value })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5"
|
||||
disabled={!n.key}>
|
||||
<option value="">选择值</option>
|
||||
{(nodeLabels[n.key] || []).map(v => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input value={t.tolerationSeconds} onChange={e => updateTol(i, { tolerationSeconds: e.target.value })}
|
||||
disabled={t.effect !== 'NoExecute'} placeholder="秒"
|
||||
className="h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono disabled:opacity-40" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeTol(i)}>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeNs(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 节点亲和性模式 */}
|
||||
{scheduleMode === 'nodeAffinity' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-slate-400 text-xs">
|
||||
请选择标签 <span className="text-slate-600">(生成 requiredDuringScheduling 规则)</span>
|
||||
</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addNs}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{labelSelectorKv.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置亲和性规则</p>
|
||||
) : labelSelectorKv.map((n, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<select value={n.key} onChange={e => updateNs(i, { key: e.target.value, value: '' })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5">
|
||||
<option value="">选择标签</option>
|
||||
{Object.entries(nodeLabels).map(([k, vals]) => (
|
||||
<option key={k} value={k}>{k} ({vals.length})</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={n.value} onChange={e => updateNs(i, { value: e.target.value })}
|
||||
className="flex-1 h-7 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1.5"
|
||||
disabled={!n.key}>
|
||||
<option value="">选择值</option>
|
||||
{(nodeLabels[n.key] || []).map(v => (
|
||||
<option key={v} value={v}>{v}</option>
|
||||
))}
|
||||
</select>
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeNs(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in new issue
Block a user