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') }