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:
attacker committed 2026-06-24 08:34:43 +00:00
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>