feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)

- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

@@ -3,10 +3,11 @@
import type {
ContainerForm, ProbeDef, LifecycleHandler,
} from './types';
import type { JsonData } from '@/types/common';
// K8s container spec → form conversion // Logan.Li
export function toContainerForm(c: any): ContainerForm {
const probe = (p: any): ProbeDef | null => {
export function toContainerForm(c: JsonData): ContainerForm {
const probe = (p: JsonData): ProbeDef | null => {
if (!p) return null;
let type: ProbeDef['type'] = 'none';
if (p.httpGet) type = 'httpGet';
@@ -26,7 +27,7 @@ export function toContainerForm(c: any): ContainerForm {
failureThreshold: p.failureThreshold,
};
};
const lh = (h: any): LifecycleHandler => {
const lh = (h: JsonData): LifecycleHandler => {
if (!h) return { type: 'none' };
let type: LifecycleHandler['type'] = 'none';
if (h.exec) type = 'exec';
@@ -48,8 +49,8 @@ export function toContainerForm(c: any): ContainerForm {
command: Array.isArray(c.command) ? c.command : [],
args: Array.isArray(c.args) ? c.args : [],
workingDir: c.workingDir || '',
env: (Array.isArray(c.env) ? c.env : []).map((e: any) => ({ name: e.name || '', value: e.value ?? '' })),
ports: (Array.isArray(c.ports) ? c.ports : []).map((p: any) => ({ name: p.name || '', containerPort: p.containerPort ?? '', protocol: p.protocol || 'TCP' })),
env: (Array.isArray(c.env) ? c.env : []).map((e: JsonData) => ({ name: e.name || '', value: e.value ?? '' })),
ports: (Array.isArray(c.ports) ? c.ports : []).map((p: JsonData) => ({ name: p.name || '', containerPort: p.containerPort ?? '', protocol: p.protocol || 'TCP' })),
resources: {
requests: c.resources?.requests || {},
limits: c.resources?.limits || {},
@@ -71,7 +72,7 @@ export function toContainerForm(c: any): ContainerForm {
capabilitiesAdd: sc.capabilities?.add || [],
capabilitiesDrop: sc.capabilities?.drop || [],
} : null,
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: any) => ({
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: JsonData) => ({
name: m.name || '', mountPath: m.mountPath || '', subPath: m.subPath || '', readOnly: m.readOnly || false,
})),
};
@@ -88,9 +89,9 @@ export function emptyContainer(name = ''): ContainerForm {
};
}
export function probeToK8s(p: ProbeDef | null | undefined): any {
export function probeToK8s(p: ProbeDef | null | undefined): JsonData {
if (!p || p.type === 'none' || !p.type) return undefined;
const out: Record<string, any> = {};
const out: JsonData = {};
if (p.type === 'httpGet') {
out.httpGet = {};
if (p.httpPath) out.httpGet.path = p.httpPath;
@@ -111,28 +112,28 @@ export function probeToK8s(p: ProbeDef | null | undefined): any {
return Object.keys(out).length > 0 ? out : undefined;
}
export function handlerToK8s(h: LifecycleHandler | undefined): any {
export function handlerToK8s(h: LifecycleHandler | undefined): JsonData {
if (!h || h.type === 'none' || !h.type) return undefined;
if (h.type === 'exec') {
const cmds = (h.execCommand || '').split('\n').map(s => s.trim()).filter(Boolean);
return cmds.length ? { exec: { command: cmds } } : undefined;
}
if (h.type === 'httpGet') {
const o: Record<string, any> = { httpGet: {} };
const o: JsonData = { httpGet: {} };
if (h.httpPath) o.httpGet.path = h.httpPath;
if (h.httpPort !== '' && h.httpPort != null) o.httpGet.port = Number(h.httpPort);
return o;
}
if (h.type === 'tcpSocket') {
const o: Record<string, any> = { tcpSocket: {} };
const o: JsonData = { tcpSocket: {} };
if (h.tcpPort !== '' && h.tcpPort != null) o.tcpSocket.port = Number(h.tcpPort);
return o;
}
return undefined;
}
export function containerToK8s(c: ContainerForm): any {
const out: Record<string, any> = { name: c.name };
export function containerToK8s(c: ContainerForm): JsonData {
const out: JsonData = { name: c.name };
if (c.image) out.image = c.image;
if (c.imagePullPolicy) out.imagePullPolicy = c.imagePullPolicy;
if (c.workingDir) out.workingDir = c.workingDir;
@@ -140,14 +141,14 @@ export function containerToK8s(c: ContainerForm): any {
if (c.args.length > 0) out.args = c.args;
if (c.env.length > 0) {
out.env = c.env.filter(e => e.name.trim()).map(e => {
const o: Record<string, any> = { name: e.name };
const o: JsonData = { name: e.name };
if (e.value != null) o.value = e.value;
return o;
});
}
if (c.ports.length > 0) {
out.ports = c.ports.filter(p => p.containerPort !== '').map(p => {
const o: Record<string, any> = { containerPort: typeof p.containerPort === 'number' ? p.containerPort : Number(p.containerPort) };
const o: JsonData = { containerPort: typeof p.containerPort === 'number' ? p.containerPort : Number(p.containerPort) };
if (p.name) o.name = p.name;
if (p.protocol) o.protocol = p.protocol;
return o;
@@ -174,14 +175,14 @@ export function containerToK8s(c: ContainerForm): any {
const rp = probeToK8s(c.readinessProbe); if (rp) out.readinessProbe = rp;
const sp = probeToK8s(c.startupProbe); if (sp) out.startupProbe = sp;
if (c.lifecycle) {
const lc: Record<string, any> = {};
const lc: JsonData = {};
const ps = handlerToK8s(c.lifecycle.postStart); if (ps) lc.postStart = ps;
const pst = handlerToK8s(c.lifecycle.preStop); if (pst) lc.preStop = pst;
if (Object.keys(lc).length > 0) out.lifecycle = lc;
}
if (c.securityContext) {
const s = c.securityContext;
const sc: Record<string, any> = {};
const sc: JsonData = {};
if (s.runAsUser !== '' && s.runAsUser != null) sc.runAsUser = Number(s.runAsUser);
if (s.runAsGroup !== '' && s.runAsGroup != null) sc.runAsGroup = Number(s.runAsGroup);
if (s.runAsNonRoot != null) sc.runAsNonRoot = s.runAsNonRoot;
@@ -199,7 +200,7 @@ export function containerToK8s(c: ContainerForm): any {
}
if (c.volumeMounts.length > 0) {
out.volumeMounts = c.volumeMounts.filter(m => m.name.trim() && m.mountPath.trim()).map(m => {
const o: Record<string, any> = { name: m.name, mountPath: m.mountPath };
const o: JsonData = { name: m.name, mountPath: m.mountPath };
if (m.subPath) o.subPath = m.subPath;
if (m.readOnly) o.readOnly = true;
return o;
@@ -1,3 +1,4 @@
import type { JsonData } from "@/types/common";
// deployment-edit/types.ts — 容器编辑器类型定义
// 作者: Logan <admin@attacker.club>
@@ -64,8 +65,8 @@ export interface DeploymentEditContainersProps {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: any | null;
onSaved: (updated?: any) => void;
manifest: JsonData | null;
onSaved: (updated?: JsonData) => void;
onCancel: () => void;
}