Files
aiops/frontend/src/components/kubernetes/k8s-yaml-utils.ts
T
attacker 34050f5188 refactor(quality): Q37 k8s-yaml-utils.ts 工具文件拆分 — 504L→451L + k8s-helpers.ts 67L 新文件
提取 7 个通用工具函数到 k8s-helpers.ts (parseScalar/STATUS_COLORS/statusColor/aggregateByImage/formatK8sTime/elapsedSeconds/formatDuration),barrel re-export 保持 8 个消费者零修改,tsc -b=0,npm build✓ (17.49s),ESLint=0/66/0,go build=0,17/17 测试通过
2026-07-10 20:39:25 +08:00

452 lines
14 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 — K8s YAML 编辑前深度清理工具
// 参考 opsx-agent clean_kubernetes_yaml() 设计,移除所有运行时/默认值/系统字段
// 作者: Logan <admin@attacker.club>
/**
* 编辑前深度清理 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<string, string[]> = {
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<string, string[]> = {
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';