// k8s-yaml-utils.ts — YAML 编辑前清理工具(借鉴 opsx-agent cleanYAML 设计) // 不依赖 js-yaml;基于文本行级解析,针对 K8s 标准 manifest 结构清理。 // 作者: Logan /** * 编辑前清理 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(items: T[]): Array { const map = new Map(); 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`; }