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

@@ -6,8 +6,9 @@ import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import { Activity, Clock, Loader2 } from 'lucide-react';
import type { JsonData } from '@/types/common';
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any[]; cronHpas: any[]; loading: string; replicas: number }) {
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: JsonData[]; cronHpas: JsonData[]; loading: string; replicas: number }) {
if (loading === 'scale') {
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
}
@@ -42,20 +43,20 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any
</TableRow>
</TableHeader>
<TableBody>
{hpas.map((hpa: any, i: number) => {
{hpas.map((hpa: JsonData, i: number) => {
const minR = hpa.spec?.minReplicas ?? hpa.minReplicas ?? 1;
const maxR = hpa.spec?.maxReplicas ?? hpa.maxReplicas ?? 10;
const metrics = hpa.spec?.metrics || hpa.metrics || [];
const curR = hpa.status?.currentReplicas ?? hpa.currentReplicas ?? 0;
const conditions = hpa.status?.conditions || hpa.conditions || [];
const ableToScale = conditions.find((c: any) => c.type === 'AbleToScale');
const ableToScale = conditions.find((c: JsonData) => c.type === 'AbleToScale');
const ok = ableToScale?.status === 'True';
return (
<TableRow key={hpa.name || i} className="border-[var(--dg-border)]">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{hpa.name || hpa.metadata?.name}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{minR} → {maxR}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">
{metrics.map((m: any, j: number) => {
{metrics.map((m: JsonData, j: number) => {
const resource = m.resource || m.object?.metric?.name || '';
const target = m.resource?.target?.averageUtilization || m.resource?.target?.averageValue || m.resource?.target?.type || '-';
return <div key={j}>{resource?.name || resource}: {target}{m.resource?.target?.averageUtilization ? '%' : ''}</div>;
@@ -98,16 +99,16 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any
</TableRow>
</TableHeader>
<TableBody>
{cronHpas.map((cron: any, i: number) => {
{cronHpas.map((cron: JsonData, i: number) => {
const jobs = cron.spec?.jobs || cron.jobs || [];
const tz = cron.spec?.timezone || 'UTC';
return (
<TableRow key={cron.name || i} className="border-[var(--dg-border)]">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cron.name || cron.metadata?.name}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{
jobs.map((j: any) => j.targetSize || j.targetReplicas || '?').join(', ')
jobs.map((j: JsonData) => j.targetSize || j.targetReplicas || '?').join(', ')
}</TableCell>
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: any) => j.schedule || '?').join(', ')}</TableCell>
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: JsonData) => j.schedule || '?').join(', ')}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
<TableCell>
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
@@ -6,8 +6,9 @@ import {
} from '@/components/ui/table';
import { RotateCcw, Loader2 } from 'lucide-react';
import { formatK8sTime } from '../k8s-yaml-utils';
import type { JsonData } from '@/types/common';
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: any[]; rollbackLoading: number | null; onRollback: (rev: any) => void }) {
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: JsonData[]; rollbackLoading: number | null; onRollback: (rev: JsonData) => void }) {
if (revisions.length === 0) {
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无历史版本</div>;
}
@@ -8,6 +8,7 @@ import {
import { formatK8sTime, statusColor } from '../k8s-yaml-utils';
import { getPodTimeline, PodConditionsRow } from './helpers';
import type { DeploymentViewProps } from './types';
import type { JsonData } from '@/types/common';
export function PodsTab({
clusterCode, pods, onShowPodLogs, onShowPodTerminal, onShowPodYaml, onShowPodFileBrowser,
@@ -61,9 +62,9 @@ export function PodsTab({
<div className="mt-3 pt-3 border-t border-[var(--dg-border)]">
<PodConditionsRow pod={pod} />
{/* Container details */}
{(pod as Record<string, any>).containers && ((pod as Record<string, any>).containers as any[]).length > 0 && (
{(pod as JsonData).containers && ((pod as JsonData).containers as JsonData[]).length > 0 && (
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2">
{((pod as Record<string, any>).containers as any[]).map((c: any, idx: number) => (
{((pod as JsonData).containers as JsonData[]).map((c: JsonData, idx: number) => (
<div key={idx} className="p-2 rounded bg-[var(--dg-bg-tertiary)] text-xs">
<div className="flex items-center justify-between">
<span className="font-medium text-[var(--dg-text-primary)]">{c.name}</span>
@@ -5,6 +5,7 @@ import {
} from '@/components/ui/table';
import { Network, ExternalLink, Loader2 } from 'lucide-react';
import type { SvcItem, IngItem } from './types';
import type { JsonData } from '@/types/common';
export function ServicesTab({ services, ingresses, loading }: { services: SvcItem[]; ingresses: IngItem[]; loading: string; namespace: string }) {
if (loading === 'services') {
@@ -76,7 +77,7 @@ export function ServicesTab({ services, ingresses, loading }: { services: SvcIte
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.className}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.hosts.join(', ')}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">
{ing.rules.flatMap((r: any) => (r.http?.paths || r.paths || []).map((p: any) => p.path || '/')).join(', ')}
{ing.rules.flatMap((r: JsonData) => (r.http?.paths || r.paths || []).map((p: JsonData) => p.path || '/')).join(', ')}
</TableCell>
</TableRow>
))}
@@ -4,18 +4,19 @@ import {
formatK8sTime, formatDuration, elapsedSeconds, STATUS_COLORS,
} from '../k8s-yaml-utils';
import type { PodItem } from './types';
import type { JsonData } from '@/types/common';
// Pod 条件时间线提取 // Logan.Li
export function getPodTimeline(pod: PodItem) {
const cond: Record<string, any>[] = (pod as Record<string, any>).conditions || (pod as Record<string, any>).pod_conditions || [];
const find = (t: string) => cond.find((c: any) => (c.type || c.Type) === t);
const ts = (c: any) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
const cond: JsonData[] = (pod as JsonData).conditions || (pod as JsonData).pod_conditions || [];
const find = (t: string) => cond.find((c: JsonData) => (c.type || c.Type) === t);
const ts = (c: JsonData) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
return {
scheduled: ts(find('PodScheduled') || {}),
initialized: ts(find('Initialized') || {}),
containersReady: ts(find('ContainersReady') || {}),
ready: ts(find('Ready') || {}),
startTime: pod.start_time || (pod as Record<string, any>).startTime,
startTime: pod.start_time || (pod as JsonData).startTime,
};
}
@@ -1,9 +1,10 @@
import type { JsonData } from "@/types/common";
// deployment-view/types.ts — Deployment 详情看板类型定义 // Logan.Li
export interface PodItem {
name: string; namespace?: string; status?: string; node_name?: string; node?: string;
ip?: string; pod_ip?: string; restart_count?: number; age?: string; created_at?: string;
start_time?: string; containers?: any[]; image?: string; images?: string[];
start_time?: string; containers?: JsonData[]; image?: string; images?: string[];
}
export interface ReplicaSetItem {
@@ -12,18 +13,18 @@ export interface ReplicaSetItem {
}
export interface SvcItem {
name: string; type: string; clusterIP: string; ports: any[]; selector: Record<string, string>; age: string;
name: string; type: string; clusterIP: string; ports: JsonData[]; selector: Record<string, string>; age: string;
}
export interface IngItem {
name: string; className: string; hosts: string[]; rules: any[]; age: string;
name: string; className: string; hosts: string[]; rules: JsonData[]; age: string;
}
export type EventItem = import('../k8s-event-utils').NormalizedEvent;
export interface DeploymentViewProps {
clusterCode: string; namespace: string; deployment: any;
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: any[];
clusterCode: string; namespace: string; deployment?: JsonData;
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: JsonData[];
onRefresh: () => void;
onShowPodLogs: (pod: PodItem) => void;
onShowPodTerminal: (pod: PodItem) => void;