Files
aiops/frontend/src/components/kubernetes/k8s-yaml-utils.ts
T
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 01:50:36 +08:00

505 lines
16 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')
);
}
/** 解析简单 "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<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 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`;
}