// k8s-yaml-utils.ts — K8s YAML 编辑前深度清理工具 // 参考 opsx-agent clean_kubernetes_yaml() 设计,移除所有运行时/默认值/系统字段 // 作者: Logan /** * 编辑前深度清理 YAML 文本(Kuboard 级精简): * - 移除 status 整块 * - 移除 metadata 噪音字段(uid/resourceVersion/managedFields/creationTimestamp 等) * - 移除 annotations 中的 kubectl 系统注解 * - 移除 labels 中的 pod-template-hash * - 移除 spec 中 K8s 默认值(dnsPolicy/restartPolicy/schedulerName 等) * - 移除容器默认值(imagePullPolicy/terminationMessagePath 等) * - 移除系统卷(kube-api-access-*)及对应 volumeMount * - 移除 Deployment 默认字段(progressDeadlineSeconds/revisionHistoryLimit) */ 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; // ===== 移除配置 ===== const METADATA_LINE_KEYS = new Set([ 'uid', 'resourceVersion', 'creationTimestamp', 'generation', 'selfLink', 'clusterName', 'generateName', 'deletionTimestamp', // managedFields 走 METADATA_BLOCK_KEYS 整块删除(含同级数组项)// Logan.Li ]); const METADATA_BLOCK_KEYS = new Set(['managedFields', 'ownerReferences', 'finalizers']); // K8s 默认值 → 值匹配时移除 const SPEC_DEFAULTS: Record = { dnsPolicy: ['ClusterFirst'], restartPolicy: ['Always'], schedulerName: ['default-scheduler'], terminationGracePeriodSeconds: ['30'], serviceAccountName: ['default'], enableServiceLinks: ['true'], automountServiceAccountToken: ['true'], progressDeadlineSeconds: ['600'], revisionHistoryLimit: ['10'], sessionAffinity: ['None'], externalTrafficPolicy: ['Cluster'], internalTrafficPolicy: ['Cluster'], ipFamilyPolicy: ['SingleStack'], allocateLoadBalancerNodePorts: ['true'], preemptionPolicy: ['PreemptLowerPriority'], priority: ['0'], }; const CONTAINER_DEFAULTS: Record = { imagePullPolicy: ['Always', 'IfNotPresent'], terminationMessagePath: ['/dev/termination-log'], terminationMessagePolicy: ['File'], stdin: ['false'], tty: ['false'], stdinOnce: ['false'], }; const SERVICE_RUNTIME_KEYS = new Set([ 'clusterIP', 'clusterIPs', 'ipFamilies', 'healthCheckNodePort', ]); const ALWAYS_REMOVE_ANNOTATIONS = new Set([ 'kubectl.kubernetes.io/last-applied-configuration', 'kubectl.kubernetes.io/change-cause', 'deployment.kubernetes.io/revision', 'deployment.kubernetes.io/desired-replicas', 'deployment.kubernetes.io/max-replicas', 'kubectl.kubernetes.io/restartedAt', 'control-plane.alpha.kubernetes.io/leader', 'autoscaling.alpha.kubernetes.io/conditions', 'kubernetes.io/config.hash', 'pv.kubernetes.io/bind-completed', 'pv.kubernetes.io/bound-by-controller', ]); const STRIP_LABEL_KEYS = new Set(['pod-template-hash', 'controller-revision-hash']); const SYSTEM_VOLUME_PREFIXES = ['kube-api-access-', 'default-token-', 'kube-']; const SYSTEM_MOUNT_PATHS = ['/var/run/secrets/kubernetes.io/serviceaccount']; // 当前作用域追踪 let inMetadata = false; let inAnnotations = false; let inSpec = false; let inTemplateSpec = false; let inContainers = false; let inInitContainers = false; let inVolumeMounts = false; let inVolumes = false; let _metadataIndent = 0; let _specIndent = 0; let templateSpecIndent = 0; function getIndent(line: string): number { return line.length - line.trimStart().length; } function extractKey(line: string): string | null { const m = line.trimStart().match(/^([a-zA-Z_][a-zA-Z0-9_./-]*)\s*:/); return m ? m[1] : null; } function hasInlineValue(line: string): boolean { const afterColon = line.trim().split(':').slice(1).join(':').trim(); return afterColon !== '' && !/^[>|]/.test(afterColon); } // 跳过一个 YAML 块(包括当前行的所有子行) function skipBlock(startIndent: number): void { i++; while (i < lines.length) { const l = lines[i]; const t = l.trimStart(); if (t === '' || t.startsWith('#')) { i++; continue; } if (getIndent(l) <= startIndent) break; i++; } } // 检查当前行是否是某个 volume 条目的开始(系统卷) function isSystemVolume(line: string): boolean { const key = extractKey(line); if (!key) return false; return SYSTEM_VOLUME_PREFIXES.some(prefix => key.startsWith(prefix)); } // 检查 volumeMount 是否指向系统挂载路径 function _isSystemMountLine(line: string): boolean { return SYSTEM_MOUNT_PATHS.some(p => line.includes(`mountPath: ${p}`)); } while (i < lines.length) { const line = lines[i]; const trimmed = line.trimStart(); const indent = getIndent(line); const key = extractKey(line); // 空行和注释保留 if (trimmed === '' || trimmed.startsWith('#')) { out.push(line); i++; continue; } // === 顶层 status: 整块移除 === if (indent === 0 && key === 'status') { skipBlock(0); continue; } // === 顶层 metadata: 跟踪作用域 === if (indent === 0 && key === 'metadata') { inMetadata = true; _metadataIndent = 0; out.push(line); i++; continue; } if (indent === 0 && key !== 'metadata') { inMetadata = false; inAnnotations = false; } // === 顶层 spec: 跟踪作用域 === if (indent === 0 && key === 'spec') { inSpec = true; _specIndent = 0; out.push(line); i++; continue; } if (indent === 0 && key !== 'spec') { inSpec = false; inTemplateSpec = false; inContainers = false; inInitContainers = false; inVolumes = false; } // === metadata 子字段 === if (inMetadata) { const metaIndent = 2; // metadata 子字段通常缩进 2 // 标注块开始/结束 if (indent === metaIndent && key === 'annotations') { inAnnotations = true; out.push(line); i++; continue; } if (indent <= metaIndent && key !== 'annotations') { inAnnotations = false; } // labels 下控制器自动标签 if (indent >= metaIndent && indent <= metaIndent + 2) { const labelKey = extractKey(line); if (labelKey && STRIP_LABEL_KEYS.has(labelKey)) { i++; continue; } } // metadata 单行字段移除 if (indent === metaIndent && key && METADATA_LINE_KEYS.has(key)) { if (hasInlineValue(line)) { i++; } else { skipBlock(metaIndent); } continue; } // metadata 块字段移除(managedFields/ownerReferences) // K8s 数组项与 key 同级缩进(如 managedFields:\n - apiVersion),需一并跳过 // Logan.Li if (indent === metaIndent && key && 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 = getIndent(l); if (ind > metaIndent || (ind === metaIndent && t.startsWith('-'))) { i++; continue; } break; } continue; } // annotations 内的系统注解 if (inAnnotations && indent > metaIndent) { if (key && ALWAYS_REMOVE_ANNOTATIONS.has(key)) { if (hasInlineValue(line)) { i++; } else { // 多行 block scalar const afterColon = trimmed.split(':').slice(1).join(':').trim(); if (/^[>|]/.test(afterColon)) { skipBlock(indent); } else { i++; } } continue; } } } // === spec 子字段 === if (inSpec) { // spec.template.spec 跟踪 if (indent === 2 && key === 'template') { out.push(line); i++; continue; } // spec.selector — 不动(关键字段) // spec.strategy — 不动 // spec 下的 K8s 默认值 / Service 运行时字段 if (indent === 2 && key) { if (SERVICE_RUNTIME_KEYS.has(key)) { if (hasInlineValue(line)) i++; else skipBlock(indent); continue; } if (SPEC_DEFAULTS[key]) { const afterColon = trimmed.split(':').slice(1).join(':').trim(); if (SPEC_DEFAULTS[key].includes(afterColon)) { i++; continue; } } } // spec 下的 nodeName(运行时分配) if (indent === 2 && key === 'nodeName') { i++; continue; } // Deployment 默认滚动策略整块移除 // Logan.Li if (indent === 2 && key === 'strategy') { const blockLines: string[] = []; let j = i; while (j < lines.length) { const l = lines[j]; const t = l.trimStart(); if (t === '' || t.startsWith('#')) { blockLines.push(l); j++; continue; } if (j > i && getIndent(l) <= indent) break; blockLines.push(l); j++; } const blockText = blockLines.join('\n'); const isDefaultRolling = blockText.includes('type: RollingUpdate') && (blockText.includes('maxUnavailable: 25%') || blockText.includes('maxUnavailable: 0')) && (blockText.includes('maxSurge: 25%') || blockText.includes('maxSurge: 1')); if (isDefaultRolling) { i = j; continue; } } // template.spec 内部 if (indent === 2 && key === 'spec') { // 这是 template.spec 的开始 inTemplateSpec = true; templateSpecIndent = indent; out.push(line); i++; continue; } } // === template.spec 内部 === if (inTemplateSpec && indent > templateSpecIndent) { // containers 数组开始 if (key === 'containers') { inContainers = true; inInitContainers = false; out.push(line); i++; continue; } if (key === 'initContainers') { inInitContainers = true; inContainers = false; out.push(line); i++; continue; } // volumes 数组开始 if (key === 'volumes') { inVolumes = true; out.push(line); i++; continue; } // template.spec 默认值 if (key && SPEC_DEFAULTS[key]) { const afterColon = trimmed.split(':').slice(1).join(':').trim(); if (SPEC_DEFAULTS[key].includes(afterColon)) { i++; continue; } } // nodeName if (key === 'nodeName') { i++; continue; } // serviceAccount (非 serviceAccountName 时的冗余字段) if (key === 'serviceAccount' && trimmed.endsWith(':')) { if (hasInlineValue(line)) { i++; } else { skipBlock(indent); } continue; } } // === containers/initContainers 内部 === if ((inContainers || inInitContainers) && indent >= 6) { // 容器字段的默认值 if (key && CONTAINER_DEFAULTS[key]) { const afterColon = trimmed.split(':').slice(1).join(':').trim(); if (CONTAINER_DEFAULTS[key].includes(afterColon)) { i++; continue; } } // volumeMounts 区域 if (key === 'volumeMounts') { inVolumeMounts = true; // 不直接输出,等下面判断每个 mount } // 系统 volumeMount 条目 if (inVolumeMounts && trimmed.startsWith('- name:')) { // 检查这个 volume mount 块内是否有系统路径 const blockLines: string[] = [line]; let j = i + 1; let isSystem = false; while (j < lines.length) { const nl = lines[j].trimStart(); if (nl === '' || nl.startsWith('#')) { blockLines.push(lines[j]); j++; continue; } if (getIndent(lines[j]) <= indent) break; if (isSystemMountPath(nl)) isSystem = true; blockLines.push(lines[j]); j++; } if (isSystem) { i = j; // 跳过整个 mount 块 continue; } // 非系统 mount,保留 inVolumeMounts = false; } } // === volumes 区域 === if (inVolumes && indent >= 4) { // 系统卷条目(- name: kube-api-access-xxx) if (trimmed.startsWith('- name:') && isSystemVolume(trimmed)) { // 跳过整个 volume 块 const blockIndent = indent; i++; while (i < lines.length) { const l = lines[i]; const t = l.trimStart(); if (t === '' || t.startsWith('#')) { i++; continue; } if (getIndent(l) <= blockIndent) break; i++; } continue; } } // 连续空行压缩 if (trimmed === '' && out.length > 0 && out[out.length - 1].trim() === '') { i++; continue; } out.push(line); i++; } // 末尾清理 const result = out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd(); return result + '\n'; } function isSystemMountPath(line: string): boolean { return line.includes('mountPath:') && ( line.includes('/var/run/secrets/kubernetes.io') || line.includes('/var/run/secrets/sa') ); } /** 解析简单 "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, '') }; } /** 状态色 */ export const STATUS_COLORS = { green: '#10b981', orange: '#f59e0b', red: '#ef4444', blue: '#326CE5', } as const; 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; } /** 按镜像聚合 */ 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 existing = map.get(img); if (existing) { existing._count += 1; } else { map.set(img, { ...it, _count: 1 }); } } return Array.from(map.values()); } /** 格式化 K8s 时间戳 */ 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)); } 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`; }