From 23e6ac3afdc27208f1a12c5836889d9708815591 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 7 Aug 2026 14:31:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(schemas):=20k8s=20toYaml=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20ports/volumes/envFrom=20=E7=AD=89=E6=95=B0=E7=BB=84?= =?UTF-8?q?-=E5=AF=B9=E8=B1=A1=E5=AD=97=E6=AE=B5=E8=A2=AB=E9=94=99?= =?UTF-8?q?=E6=B8=B2=E4=B8=BA=20dict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toYaml(obj, indent=0) 旧版递归缩进计算错位,导致: ports: ports: <- 多余的父 key 行 - name: http containerPort: 5099 被 pyyaml 反解为 {0:{...}} 而不是 list, kubectl apply 报: cannot unmarshal object into Go struct field Container.spec.template.spec.containers.ports of type []v1.ContainerPort 修复: - 重写为 baseIndent 字符串缩进版, 数组-对象分支递归渲染后第一行加 '- ' - 新增 yamlScalar(v) 处理 number/boolean/特殊字符安全引号 - 新增 isEmptyMapping(o) 把 emptyDir:{} 等空对象渲染为 {} - 20 个 k8s-*.js schema 全部统一替换 验证: - npm run build 通过 - PyYAML 反序列化 ports/volumes/envFrom/tolerations 等 9 个数组-对象字段全部为 list - 全部 20 个 schema E2E OK --- src/schemas/k8s-clusterrole.js | 63 +++++++++++++------ src/schemas/k8s-clusterrolebinding.js | 63 +++++++++++++------ src/schemas/k8s-configmap.js | 63 +++++++++++++------ src/schemas/k8s-cronjob.js | 68 +++++++++++++++------ src/schemas/k8s-daemonset.js | 69 ++++++++++++++------- src/schemas/k8s-deployment.js | 88 ++++++++++++++++++++------- src/schemas/k8s-ingress.js | 63 +++++++++++++------ src/schemas/k8s-job.js | 68 +++++++++++++++------ src/schemas/k8s-namespace.js | 63 +++++++++++++------ src/schemas/k8s-networkpolicy.js | 63 +++++++++++++------ src/schemas/k8s-pod.js | 69 ++++++++++++++------- src/schemas/k8s-pv.js | 63 +++++++++++++------ src/schemas/k8s-pvc.js | 63 +++++++++++++------ src/schemas/k8s-role.js | 63 +++++++++++++------ src/schemas/k8s-rolebinding.js | 63 +++++++++++++------ src/schemas/k8s-secret.js | 63 +++++++++++++------ src/schemas/k8s-service.js | 63 +++++++++++++------ src/schemas/k8s-serviceaccount.js | 63 +++++++++++++------ src/schemas/k8s-statefulset.js | 69 ++++++++++++++------- src/schemas/k8s-storageclass.js | 63 +++++++++++++------ 20 files changed, 947 insertions(+), 366 deletions(-) diff --git a/src/schemas/k8s-clusterrole.js b/src/schemas/k8s-clusterrole.js index 10f7bb3..fcbfaae 100644 --- a/src/schemas/k8s-clusterrole.js +++ b/src/schemas/k8s-clusterrole.js @@ -76,36 +76,65 @@ export const k8sClusterRoleSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-clusterrolebinding.js b/src/schemas/k8s-clusterrolebinding.js index 9232f25..321291c 100644 --- a/src/schemas/k8s-clusterrolebinding.js +++ b/src/schemas/k8s-clusterrolebinding.js @@ -75,36 +75,65 @@ export const k8sClusterRoleBindingSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-configmap.js b/src/schemas/k8s-configmap.js index b373659..0d3798a 100644 --- a/src/schemas/k8s-configmap.js +++ b/src/schemas/k8s-configmap.js @@ -90,36 +90,65 @@ export const k8sConfigMapSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-cronjob.js b/src/schemas/k8s-cronjob.js index 90eabae..ebe2026 100644 --- a/src/schemas/k8s-cronjob.js +++ b/src/schemas/k8s-cronjob.js @@ -237,31 +237,63 @@ export const k8sCronJobSchema = { // ======================== YAML 生成器 ======================== -function toYaml(obj, indent) { - indent = indent || 0 - var lines = [] - var prefix = '' - for (var p = 0; p < indent; p++) prefix += ' ' - var keys = Object.keys(obj) - for (var ki = 0; ki < keys.length; ki++) { - var key = keys[ki] - var value = obj[key] + + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] + for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(prefix + key + ':') - for (var ai = 0; ai < value.length; ai++) { - var item = value[ai] - if (typeof item === 'object' && item !== null) { - lines.push(renderArrayObject(item, indent + 1)) + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue + const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() + } + lines.push(...subLines) } else { - lines.push(prefix + ' - ' + item) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(prefix + key + ':') - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(prefix + key + ': ' + value) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } return lines.join('\n') diff --git a/src/schemas/k8s-daemonset.js b/src/schemas/k8s-daemonset.js index 3bbf253..691bdb3 100644 --- a/src/schemas/k8s-daemonset.js +++ b/src/schemas/k8s-daemonset.js @@ -357,40 +357,63 @@ export function generateK8sDaemonSetYaml(config) { return header + '\n' + toYaml(daemonset) } -function toYaml(obj, indent = 0) { + + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { const lines = [] - const prefix = ' '.repeat(indent) for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`) - if (typeof first[1] === 'object') { - lines.push(toYaml(first[1], indent + 3)) - } - for (let i = 1; i < entries.length; i++) { - const [k, v] = entries[i] - if (typeof v === 'object') { - lines.push(`${prefix} ${k}:`) - lines.push(toYaml(v, indent + 4)) - } else { - lines.push(`${prefix} ${k}: ${v}`) - } + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } return lines.join('\n') diff --git a/src/schemas/k8s-deployment.js b/src/schemas/k8s-deployment.js index 585e68f..37ebc06 100644 --- a/src/schemas/k8s-deployment.js +++ b/src/schemas/k8s-deployment.js @@ -433,40 +433,82 @@ export function generateK8sDeploymentYaml(config) { return header + '\n' + toYaml(deployment) } -function toYaml(obj, indent = 0) { +// 序列化值为带引号的 YAML 标量字符串 + +// 过滤掉空值/null +function clean(obj) { + if (Array.isArray(obj)) return obj.map(clean).filter(v => v !== undefined) + if (obj && typeof obj === 'object') { + const out = {} + for (const [k, v] of Object.entries(obj)) { + if (v === null || v === undefined || v === '') continue + const cv = clean(v) + if (cv === undefined) continue + out[k] = cv + } + return Object.keys(out).length ? out : undefined + } + return obj +} + +// 通用递归 YAML 序列化器:父级 + 本行缩进 + 子级缩进 + + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { const lines = [] - const prefix = ' '.repeat(indent) for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`) - if (typeof first[1] === 'object') { - lines.push(toYaml(first[1], indent + 3)) - } - for (let i = 1; i < entries.length; i++) { - const [k, v] = entries[i] - if (typeof v === 'object') { - lines.push(`${prefix} ${k}:`) - lines.push(toYaml(v, indent + 4)) - } else { - lines.push(`${prefix} ${k}: ${v}`) - } + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } return lines.join('\n') diff --git a/src/schemas/k8s-ingress.js b/src/schemas/k8s-ingress.js index da03c17..d03340b 100644 --- a/src/schemas/k8s-ingress.js +++ b/src/schemas/k8s-ingress.js @@ -136,36 +136,65 @@ export const k8sIngressSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-job.js b/src/schemas/k8s-job.js index e98ecd6..3eb506c 100644 --- a/src/schemas/k8s-job.js +++ b/src/schemas/k8s-job.js @@ -208,31 +208,63 @@ export const k8sJobSchema = { // ======================== YAML 生成器 ======================== -function toYaml(obj, indent) { - indent = indent || 0 - var lines = [] - var prefix = '' - for (var p = 0; p < indent; p++) prefix += ' ' - var keys = Object.keys(obj) - for (var ki = 0; ki < keys.length; ki++) { - var key = keys[ki] - var value = obj[key] + + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] + for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(prefix + key + ':') - for (var ai = 0; ai < value.length; ai++) { - var item = value[ai] - if (typeof item === 'object' && item !== null) { - lines.push(renderArrayObject(item, indent + 1)) + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue + const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() + } + lines.push(...subLines) } else { - lines.push(prefix + ' - ' + item) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(prefix + key + ':') - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(prefix + key + ': ' + value) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } return lines.join('\n') diff --git a/src/schemas/k8s-namespace.js b/src/schemas/k8s-namespace.js index 2c27613..59adff4 100644 --- a/src/schemas/k8s-namespace.js +++ b/src/schemas/k8s-namespace.js @@ -48,36 +48,65 @@ export const k8sNamespaceSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-networkpolicy.js b/src/schemas/k8s-networkpolicy.js index 3ae69a0..964315c 100644 --- a/src/schemas/k8s-networkpolicy.js +++ b/src/schemas/k8s-networkpolicy.js @@ -114,36 +114,65 @@ export const k8sNetworkPolicySchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-pod.js b/src/schemas/k8s-pod.js index 5d14973..64c7169 100644 --- a/src/schemas/k8s-pod.js +++ b/src/schemas/k8s-pod.js @@ -445,40 +445,63 @@ export function generateK8sPodYaml(config) { return header + '\n' + toYaml(pod) } -function toYaml(obj, indent = 0) { + + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { const lines = [] - const prefix = ' '.repeat(indent) for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`) - if (typeof first[1] === 'object') { - lines.push(toYaml(first[1], indent + 3)) - } - for (let i = 1; i < entries.length; i++) { - const [k, v] = entries[i] - if (typeof v === 'object') { - lines.push(`${prefix} ${k}:`) - lines.push(toYaml(v, indent + 4)) - } else { - lines.push(`${prefix} ${k}: ${v}`) - } + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } return lines.join('\n') diff --git a/src/schemas/k8s-pv.js b/src/schemas/k8s-pv.js index 3fb5823..93601cc 100644 --- a/src/schemas/k8s-pv.js +++ b/src/schemas/k8s-pv.js @@ -125,36 +125,65 @@ export const k8sPvSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-pvc.js b/src/schemas/k8s-pvc.js index b7f6363..309d124 100644 --- a/src/schemas/k8s-pvc.js +++ b/src/schemas/k8s-pvc.js @@ -72,36 +72,65 @@ export const k8sPvcSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-role.js b/src/schemas/k8s-role.js index c975bc8..b673be0 100644 --- a/src/schemas/k8s-role.js +++ b/src/schemas/k8s-role.js @@ -81,36 +81,65 @@ export const k8sRoleSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-rolebinding.js b/src/schemas/k8s-rolebinding.js index f58021e..b1e08b8 100644 --- a/src/schemas/k8s-rolebinding.js +++ b/src/schemas/k8s-rolebinding.js @@ -82,36 +82,65 @@ export const k8sRoleBindingSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-secret.js b/src/schemas/k8s-secret.js index ae25a84..c932d0e 100644 --- a/src/schemas/k8s-secret.js +++ b/src/schemas/k8s-secret.js @@ -98,36 +98,65 @@ export const k8sSecretSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-service.js b/src/schemas/k8s-service.js index ef1c679..e46cc65 100644 --- a/src/schemas/k8s-service.js +++ b/src/schemas/k8s-service.js @@ -134,36 +134,65 @@ export const k8sServiceSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-serviceaccount.js b/src/schemas/k8s-serviceaccount.js index ceee96f..33f6914 100644 --- a/src/schemas/k8s-serviceaccount.js +++ b/src/schemas/k8s-serviceaccount.js @@ -62,36 +62,65 @@ export const k8sServiceAccountSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') } diff --git a/src/schemas/k8s-statefulset.js b/src/schemas/k8s-statefulset.js index 7680f2e..72da863 100644 --- a/src/schemas/k8s-statefulset.js +++ b/src/schemas/k8s-statefulset.js @@ -424,40 +424,63 @@ export function generateK8sStatefulSetYaml(config) { return header + '\n' + toYaml(statefulset) } -function toYaml(obj, indent = 0) { + + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { const lines = [] - const prefix = ' '.repeat(indent) for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`) - if (typeof first[1] === 'object') { - lines.push(toYaml(first[1], indent + 3)) - } - for (let i = 1; i < entries.length; i++) { - const [k, v] = entries[i] - if (typeof v === 'object') { - lines.push(`${prefix} ${k}:`) - lines.push(toYaml(v, indent + 4)) - } else { - lines.push(`${prefix} ${k}: ${v}`) - } + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } return lines.join('\n') diff --git a/src/schemas/k8s-storageclass.js b/src/schemas/k8s-storageclass.js index 10c3e84..6d856dd 100644 --- a/src/schemas/k8s-storageclass.js +++ b/src/schemas/k8s-storageclass.js @@ -103,36 +103,65 @@ export const k8sStorageClassSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) + + +function yamlScalar(v) { + if (v === null || v === undefined) return '' + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + const s = String(v) + if (/[:#\n"'`]|^[\s\-]|[\s]$/.test(s) || /^(true|false|null|yes|no|on|off|~)$/i.test(s)) { + return JSON.stringify(s) + } + return s +} + +function isEmptyMapping(o) { + if (!o || typeof o !== 'object' || Array.isArray(o)) return false + return Object.values(o).every(v => v === null || v === undefined || v === '' || (typeof v === 'object' && isEmptyMapping(v))) +} + +function toYaml(obj, baseIndent = '') { + const lines = [] for (const [key, value] of Object.entries(obj)) { if (value === null || value === undefined || value === '') continue - if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { - if (typeof item === 'object' && item !== null) { + const items = value.filter(v => v !== null && v !== undefined && v !== '') + if (!items.length) continue + lines.push(`${baseIndent}${key}:`) + for (const item of items) { + if (item && typeof item === 'object' && !Array.isArray(item)) { + if (isEmptyMapping(item)) continue const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') - if (entries.length === 0) continue - const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) + if (!entries.length) continue + const childIndent = baseIndent + ' ' + const subAllIndent = childIndent + ' ' + const subObj = {} + for (const [k, v] of entries) subObj[k] = v + const subRendered = toYaml(subObj, subAllIndent) + const subLines = subRendered.split('\n') + if (subLines[0].startsWith(subAllIndent)) { + subLines[0] = childIndent + '- ' + subLines[0].slice(subAllIndent.length) + } else { + subLines[0] = childIndent + '- ' + subLines[0].trimStart() } + lines.push(...subLines) } else { - lines.push(`${prefix} - ${item}`) + lines.push(`${baseIndent} - ${yamlScalar(item)}`) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + if (isEmptyMapping(value)) { + lines.push(`${baseIndent}${key}: {}`) + continue + } + lines.push(`${baseIndent}${key}:`) + const childIndent = baseIndent + ' ' + lines.push(toYaml(value, childIndent)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`) } } - return lines.join('\n') }