- 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 回归通过
214 lines
9.0 KiB
TypeScript
214 lines
9.0 KiB
TypeScript
// container-utils.ts — K8s container spec ↔ form 转换工具
|
|
// 作者: Logan <admin@attacker.club>
|
|
import type {
|
|
ContainerForm, ProbeDef, LifecycleHandler,
|
|
} from './types';
|
|
import type { JsonData } from '@/types/common';
|
|
|
|
// K8s container spec → form conversion // Logan.Li
|
|
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';
|
|
else if (p.tcpSocket) type = 'tcpSocket';
|
|
else if (p.exec) type = 'exec';
|
|
return {
|
|
type,
|
|
httpPath: p.httpGet?.path || '',
|
|
httpPort: p.httpGet?.port ?? '',
|
|
httpHost: p.httpGet?.host || '',
|
|
tcpPort: p.tcpSocket?.port ?? '',
|
|
execCommand: Array.isArray(p.exec?.command) ? p.exec.command.join('\n') : (p.exec?.command || ''),
|
|
initialDelaySeconds: p.initialDelaySeconds,
|
|
periodSeconds: p.periodSeconds,
|
|
timeoutSeconds: p.timeoutSeconds,
|
|
successThreshold: p.successThreshold,
|
|
failureThreshold: p.failureThreshold,
|
|
};
|
|
};
|
|
const lh = (h: JsonData): LifecycleHandler => {
|
|
if (!h) return { type: 'none' };
|
|
let type: LifecycleHandler['type'] = 'none';
|
|
if (h.exec) type = 'exec';
|
|
else if (h.httpGet) type = 'httpGet';
|
|
else if (h.tcpSocket) type = 'tcpSocket';
|
|
return {
|
|
type,
|
|
execCommand: Array.isArray(h.exec?.command) ? h.exec.command.join('\n') : '',
|
|
httpPath: h.httpGet?.path || '',
|
|
httpPort: h.httpGet?.port ?? '',
|
|
tcpPort: h.tcpSocket?.port ?? '',
|
|
};
|
|
};
|
|
const sc = c.securityContext;
|
|
return {
|
|
name: c.name || '',
|
|
image: c.image || '',
|
|
imagePullPolicy: c.imagePullPolicy || '',
|
|
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: 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 || {},
|
|
},
|
|
livenessProbe: probe(c.livenessProbe),
|
|
readinessProbe: probe(c.readinessProbe),
|
|
startupProbe: probe(c.startupProbe),
|
|
lifecycle: c.lifecycle ? {
|
|
postStart: c.lifecycle.postStart ? lh(c.lifecycle.postStart) : undefined,
|
|
preStop: c.lifecycle.preStop ? lh(c.lifecycle.preStop) : undefined,
|
|
} : null,
|
|
securityContext: sc ? {
|
|
runAsUser: sc.runAsUser,
|
|
runAsGroup: sc.runAsGroup,
|
|
runAsNonRoot: sc.runAsNonRoot,
|
|
privileged: sc.privileged,
|
|
readOnlyRootFilesystem: sc.readOnlyRootFilesystem,
|
|
allowPrivilegeEscalation: sc.allowPrivilegeEscalation,
|
|
capabilitiesAdd: sc.capabilities?.add || [],
|
|
capabilitiesDrop: sc.capabilities?.drop || [],
|
|
} : null,
|
|
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: JsonData) => ({
|
|
name: m.name || '', mountPath: m.mountPath || '', subPath: m.subPath || '', readOnly: m.readOnly || false,
|
|
})),
|
|
};
|
|
}
|
|
|
|
// form → K8s container spec // Logan.Li
|
|
export function emptyContainer(name = ''): ContainerForm {
|
|
return {
|
|
name, image: '', imagePullPolicy: 'IfNotPresent',
|
|
command: [], args: [], workingDir: '',
|
|
env: [], ports: [], resources: {},
|
|
livenessProbe: null, readinessProbe: null, startupProbe: null,
|
|
lifecycle: null, securityContext: null, volumeMounts: [],
|
|
};
|
|
}
|
|
|
|
export function probeToK8s(p: ProbeDef | null | undefined): JsonData {
|
|
if (!p || p.type === 'none' || !p.type) return undefined;
|
|
const out: JsonData = {};
|
|
if (p.type === 'httpGet') {
|
|
out.httpGet = {};
|
|
if (p.httpPath) out.httpGet.path = p.httpPath;
|
|
if (p.httpPort !== '' && p.httpPort != null) out.httpGet.port = typeof p.httpPort === 'number' ? p.httpPort : Number(p.httpPort);
|
|
if (p.httpHost) out.httpGet.host = p.httpHost;
|
|
} else if (p.type === 'tcpSocket') {
|
|
out.tcpSocket = {};
|
|
if (p.tcpPort !== '' && p.tcpPort != null) out.tcpSocket.port = typeof p.tcpPort === 'number' ? p.tcpPort : Number(p.tcpPort);
|
|
} else if (p.type === 'exec') {
|
|
const cmds = (p.execCommand || '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
if (cmds.length) out.exec = { command: cmds };
|
|
}
|
|
if (p.initialDelaySeconds != null) out.initialDelaySeconds = p.initialDelaySeconds;
|
|
if (p.periodSeconds != null) out.periodSeconds = p.periodSeconds;
|
|
if (p.timeoutSeconds != null) out.timeoutSeconds = p.timeoutSeconds;
|
|
if (p.successThreshold != null) out.successThreshold = p.successThreshold;
|
|
if (p.failureThreshold != null) out.failureThreshold = p.failureThreshold;
|
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
}
|
|
|
|
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: 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: JsonData = { tcpSocket: {} };
|
|
if (h.tcpPort !== '' && h.tcpPort != null) o.tcpSocket.port = Number(h.tcpPort);
|
|
return o;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
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;
|
|
if (c.command.length > 0) out.command = c.command;
|
|
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: 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: 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;
|
|
});
|
|
}
|
|
const req = c.resources.requests;
|
|
const lim = c.resources.limits;
|
|
const hasReq = req && Object.values(req).some(v => v && v.trim());
|
|
const hasLim = lim && Object.values(lim).some(v => v && v.trim());
|
|
if (hasReq || hasLim) {
|
|
out.resources = {};
|
|
if (hasReq) {
|
|
out.resources.requests = {};
|
|
if (req!.cpu?.trim()) out.resources.requests.cpu = req!.cpu;
|
|
if (req!.memory?.trim()) out.resources.requests.memory = req!.memory;
|
|
}
|
|
if (hasLim) {
|
|
out.resources.limits = {};
|
|
if (lim!.cpu?.trim()) out.resources.limits.cpu = lim!.cpu;
|
|
if (lim!.memory?.trim()) out.resources.limits.memory = lim!.memory;
|
|
}
|
|
}
|
|
const lp = probeToK8s(c.livenessProbe); if (lp) out.livenessProbe = lp;
|
|
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: 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: 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;
|
|
if (s.privileged != null) sc.privileged = s.privileged;
|
|
if (s.readOnlyRootFilesystem != null) sc.readOnlyRootFilesystem = s.readOnlyRootFilesystem;
|
|
if (s.allowPrivilegeEscalation != null) sc.allowPrivilegeEscalation = s.allowPrivilegeEscalation;
|
|
const capAdd = (s.capabilitiesAdd || []).filter(Boolean);
|
|
const capDrop = (s.capabilitiesDrop || []).filter(Boolean);
|
|
if (capAdd.length || capDrop.length) {
|
|
sc.capabilities = {};
|
|
if (capAdd.length) sc.capabilities.add = capAdd;
|
|
if (capDrop.length) sc.capabilities.drop = capDrop;
|
|
}
|
|
if (Object.keys(sc).length > 0) out.securityContext = sc;
|
|
}
|
|
if (c.volumeMounts.length > 0) {
|
|
out.volumeMounts = c.volumeMounts.filter(m => m.name.trim() && m.mountPath.trim()).map(m => {
|
|
const o: JsonData = { name: m.name, mountPath: m.mountPath };
|
|
if (m.subPath) o.subPath = m.subPath;
|
|
if (m.readOnly) o.readOnly = true;
|
|
return o;
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// String array helper (command/args editing) // Logan.Li
|
|
export function arrToText(arr: string[]): string { return arr.join('\n'); }
|
|
export function textToArr(text: string): string[] { return text.split('\n').map(s => s.trim()).filter(Boolean); } |