Files
aiops/frontend/src/components/kubernetes/k8s-yaml-utils.ts
T
attacker ce438c9f05 fix: K8s 集群添加缺少 code 必填字段
- KubernetesClusterDialog: 新增 code 表单字段
- 名称变更自动生成 code(小写+连字符)
- handleSubmit 增加 code 校验 + toast 提示
- payload 增加 code 字段(后端 binding:required)
2026-06-21 09:50:07 +00:00

195 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// k8s-yaml-utils.ts — YAML 编辑前清理工具(借鉴 opsx-agent cleanYAML 设计)
// 不依赖 js-yaml;基于文本行级解析,针对 K8s 标准 manifest 结构清理。
// 作者: Logan <admin@attacker.club>
/**
* 编辑前清理 YAML 文本:
* - 移除 status 整块(K8s 服务端字段)
* - 移除 metadata 中的只读/噪音字段:uid / resourceVersion / managedFields /
* creationTimestamp / generation / selfLink / ownerReferences / resourceVersion
* - 移除 annotations 中的 kubectl.kubernetes.io/last-applied-configuration
* - 规范化行尾,保留可读缩进
*/
export function cleanYAML(yaml: string): string {
if (!yaml) return '';
const lines = yaml.replace(/\r\n/g, '\n').split('\n');
const out: string[] = [];
let i = 0;
// metadata 下需要整块移除的字段(值为多行块或对象)
const METADATA_BLOCK_KEYS = new Set([
'managedFields',
'ownerReferences',
]);
// metadata 下需要单行移除的字段
const METADATA_LINE_KEYS = new Set([
'uid',
'resourceVersion',
'creationTimestamp',
'generation',
'selfLink',
'clusterName',
'managedFields', // 兜底
]);
while (i < lines.length) {
const line = lines[i];
const trimmed = line.trimStart();
const indent = line.length - trimmed.length;
// 1) 顶层 status: 整块移除(indent === 0)
if (indent === 0 && /^status\s*:/.test(trimmed)) {
i++;
// 跳过所有后续属于 status 块的行(缩进 > 0 或空行),直到下一个顶层 key
while (i < lines.length) {
const l = lines[i];
const t = l.trimStart();
if (t === '' || t.startsWith('#')) { i++; continue; }
const ind = l.length - t.length;
if (ind === 0) break; // 回到顶层
i++;
}
continue;
}
// 2) metadata 子字段清理
// 匹配形如 " uid: xxx" 或 " managedFields:" 等
const metaKeyMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:/);
if (metaKeyMatch && indent >= 2 && indent <= 6) {
const key = metaKeyMatch[1];
// 单行移除
if (METADATA_LINE_KEYS.has(key)) {
// 若该行值非空(如 "uid: xxx"),直接跳过该行
// 若该行值为空(如 "managedFields:"),需要跳过其下属缩进块
const afterColon = trimmed.slice(metaKeyMatch[0].length).trim();
if (afterColon === '') {
i++;
while (i < lines.length) {
const l = lines[i];
const t = l.trimStart();
if (t === '' || t.startsWith('#')) { i++; continue; }
const ind = l.length - t.length;
if (ind <= indent) break;
i++;
}
} else {
i++;
}
continue;
}
// managedFields 块移除(即使有内联内容也整块跳过)
if (METADATA_BLOCK_KEYS.has(key)) {
i++;
while (i < lines.length) {
const l = lines[i];
const t = l.trimStart();
if (t === '' || t.startsWith('#')) { i++; continue; }
const ind = l.length - t.length;
if (ind <= indent) break;
i++;
}
continue;
}
}
// 3) annotations 内 last-applied-configuration 移除
// 形如 " kubectl.kubernetes.io/last-applied-configuration: >-"
// 可能跨多行(block scalar)
if (/kubectl\.kubernetes\.io\/last-applied-configuration\s*:/.test(trimmed)) {
const afterColon = trimmed.split(':').slice(1).join(':').trim();
// 多行 block scalar 标记:>- >- | |- 等
const isBlockScalar = /^[>|]/.test(afterColon);
i++;
if (isBlockScalar) {
while (i < lines.length) {
const l = lines[i];
const t = l.trimStart();
if (t === '') { i++; continue; }
const ind = l.length - t.length;
if (ind <= indent) break;
i++;
}
}
continue;
}
out.push(line);
i++;
}
// 末尾保持一个换行
let result = out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
return result + '\n';
}
/** 解析简单 "key: value" 行(顶层或指定缩进),用于表单回填兜底。 */
export function parseScalar(line: string): { key: string; value: string } | null {
const m = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
if (!m) return null;
return { key: m[1], value: m[2].trim().replace(/^["']|["']$/g, '') };
}
/** 状态色(与 opsx-agent 一致) */
export const STATUS_COLORS = {
green: '#10b981',
orange: '#f59e0b',
red: '#ef4444',
blue: '#326CE5', // Kubernetes 官方蓝
} as const;
/** 根据 Pod/Deployment 状态返回对应色值 */
export function statusColor(status: string | undefined): string {
if (!status) return '#64748b';
const s = status.toLowerCase();
if (['running', 'available', 'ready', 'active', 'completed', 'succeeded'].includes(s)) return STATUS_COLORS.green;
if (['pending', 'updating', 'progressing', 'containercreating'].includes(s)) return STATUS_COLORS.orange;
if (['failed', 'error', 'crashloopbackoff', 'imagepullbackoff', 'errimageneverpull'].includes(s)) return STATUS_COLORS.red;
return STATUS_COLORS.blue;
}
/** 按镜像聚合 ReplicaSet / Revision 列表(同镜像合并展示) */
export function aggregateByImage<T extends { image?: string; images?: string[] }>(items: T[]): Array<T & { _count: number }> {
const map = new Map<string, T & { _count: number }>();
for (const it of items) {
const img = it.image || (Array.isArray(it.images) ? it.images[0] : '') || 'unknown';
const key = img;
const existing = map.get(key);
if (existing) {
existing._count += 1;
} else {
map.set(key, { ...it, _count: 1 });
}
}
return Array.from(map.values());
}
/** 格式化 K8s 时间戳(ISO / RFC3339)为本地可读格式 */
export function formatK8sTime(ts: string | undefined | null): string {
if (!ts) return '-';
const d = new Date(ts);
if (isNaN(d.getTime())) return ts;
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
/** 计算两个时间戳之间的相对耗时(秒/分/时),用于"调度耗时""就绪耗时"展示 */
export function elapsedSeconds(start?: string | null, end?: string | null): number | null {
if (!start || !end) return null;
const s = new Date(start).getTime();
const e = new Date(end).getTime();
if (isNaN(s) || isNaN(e) || e < s) return null;
return Math.max(0, Math.round((e - s) / 1000));
}
/** 把秒数格式化为 "1m23s" / "2h5m" / "45s" */
export function formatDuration(sec: number | null | undefined): string {
if (sec == null) return '-';
if (sec < 60) return `${sec}s`;
if (sec < 3600) return `${Math.floor(sec / 60)}m${sec % 60}s`;
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
return `${h}h${m}m`;
}