// 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') ); } // Barrel re-export: 通用工具函数已提取到 k8s-helpers.ts // 保留 re-export 以兼容现有 import 路径(8 个消费者无需修改) export { parseScalar, STATUS_COLORS, statusColor, aggregateByImage, formatK8sTime, elapsedSeconds, formatDuration, } from './k8s-helpers';