2 Commits

Author SHA1 Message Date
Your Name 11f86bde47 feat(schemas): 新增 GitLab CI 与 Jenkinsfile 流水线生成器
背景: 现有 gitlab.js / jenkins.js 仅生成服务端配置文件
      (gitlab.rb / jenkins.yaml), 不能直接产出 CI 流水线。

新增:
- src/schemas/gitlab-ci.js      生成 .gitlab-ci.yml (声明式流水线 YAML)
- src/schemas/jenkins-pipeline.js  生成 Jenkinsfile (Declarative 2.x Groovy)
- src/schemas/index.js 注册到 schemaMap / generatorMap / CI/CD 分类

GitLab CI 覆盖:
- stages 自定义 / 默认切换
- global variables (keyvalue) / default.image / workflow policy
- 4 阶段 Job (lint/build/test/deploy), 每阶段可独立开关
- cache 块 (key.files + paths + policy)
- include 高级用户自填
- 修正后 toYaml 保证所有 list 字段 (stages / script / services /
  artifacts.paths / needs / rules / cache.paths) 在 PyYAML 反解为 list

Jenkinsfile 覆盖:
- agent: none/any/label/docker(支持 args+reuseNode)/kubernetes(Pod YAML)
- tools (maven/jdk/gradle/nodejs 自动安装)
- environment env vars (keyvalue)
- options (disableConcurrent / timeout / timestamps / ansiColor /
  buildDiscarder / skipDefaultCheckout)
- triggers (githubPush / pollSCM / cron)
- 5 stages (Checkout/Lint/Build/Test/Deploy), 每个可独立开关
- input 人工确认 gate + when { branch ... }
- 顶级 post (always/success/failure/unstable/cleanup)
- 缩进严格 2 空格层级; 大括号平衡验证 29/29

构建:
- npm run build 通过 (dist/assets/index-BLj3d4DO.js)
2026-08-07 15:43:52 +08:00
Your Name 23e6ac3afd fix(schemas): k8s toYaml 修复 ports/volumes/envFrom 等数组-对象字段被错渲为 dict
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
2026-08-07 14:31:19 +08:00
23 changed files with 1781 additions and 367 deletions
+428
View File
@@ -0,0 +1,428 @@
// GitLab CI 流水线代码生成器 for ConfTemplate
// 生成符合 .gitlab-ci.yml v14+ 规范的声明式流水线 YAML
export const gitlabCiSchema = {
id: 'gitlab-ci',
name: 'GitLab CI',
icon: 'Files',
category: 'CI/CD',
description: '生成 .gitlab-ci.yml 声明式流水线代码',
format: 'yaml',
fileName: '.gitlab-ci.yml',
groups: [
// ─────────────── 全局变量 ───────────────
{
title: '全局变量',
fields: [
{ key: 'stagesEnabled', label: '自定义 stages', type: 'switch', default: true,
tip: '关闭则使用 GitLab 默认 stages (build / test / deploy)' },
{ key: 'stages', label: 'Stages 顺序', type: 'text',
default: 'lint,build,test,deploy',
placeholder: 'lint,build,test,deploy',
dependsOn: { key: 'stagesEnabled', value: true },
tip: '逗号分隔,按执行顺序排列' },
{ key: 'defaultImage', label: '默认镜像', type: 'text',
default: 'alpine:3.20',
tip: '未在 job 中指定 image 时使用' },
{ key: 'variablesMode', label: '全局变量', type: 'keyvalue', default: [],
tip: '所有 job 都可用的 CI/CD 变量,例如 PROJECT_NAME / DOCKER_REGISTRY' },
{ key: 'workflowPolicy', label: 'workflow 策略', type: 'select', default: 'auto', options: [
{ label: 'auto (CI 默认可跑)', value: 'auto' },
{ label: 'always (任何情况都跑)', value: 'always' },
{ label: 'on_success (上游成功才跑)', value: 'on_success' },
{ label: 'manual (需要手动触发下游)', value: 'manual' },
{ label: 'none (禁止流水线,不允许触发)', value: 'none' },
], tip: 'GitLab Premium 有 start_in / rules 输入参数' },
{ key: 'workflowInputs', label: 'workflow:inputs 声明', type: 'code',
language: 'yaml', default: '',
tip: '可选:手动指定 workflow:inputs: 示例 → 用 [[inputs]] 上半部 YAML 多行块;留空跳过' },
{ key: 'includeMode', label: 'Include 模板块(高级)', type: 'code',
language: 'yaml', default: '',
tip: '需要 local / template / remote 引用模板时直接在这里补全完整 include 块;留空则不输出' },
],
},
// ─────────────── 构建 Job ───────────────
{
title: 'Job · lint (静态检查)',
fields: [
{ key: 'enableLint', label: '启用 lint 阶段', type: 'switch', default: true,
tip: '关闭则完全不渲染 lint job(连 stage 都没出现)' },
{ key: 'lintName', label: 'Job 名', type: 'text', default: 'lint',
dependsOn: { key: 'enableLint', value: true } },
{ key: 'lintImage', label: '镜像', type: 'text', default: 'alpine:3.20',
dependsOn: { key: 'enableLint', value: true } },
{ key: 'lintStage', label: 'Stage', type: 'text', default: 'lint',
dependsOn: { key: 'enableLint', value: true } },
{ key: 'lintScript', label: 'script', type: 'code', language: 'shell', default: 'echo "🔍 running lint"\n# npm run lint\n# ruff check .\n# shellcheck *.sh',
dependsOn: { key: 'enableLint', value: true } },
{ key: 'lintAllowFailure', label: 'allow_failure', type: 'switch', default: true,
dependsOn: { key: 'enableLint', value: true } },
],
},
{
title: 'Job · build (打包产物)',
fields: [
{ key: 'enableBuild', label: '启用 build 阶段', type: 'switch', default: true },
{ key: 'buildName', label: 'Job 名', type: 'text', default: 'build',
dependsOn: { key: 'enableBuild', value: true } },
{ key: 'buildImage', label: '镜像', type: 'text',
default: 'docker:27-cli',
dependsOn: { key: 'enableBuild', value: true } },
{ key: 'buildStage', label: 'Stage', type: 'text', default: 'build',
dependsOn: { key: 'enableBuild', value: true } },
{ key: 'buildServices', label: 'services (附带容器)', type: 'keyvalue', default: [
{ key: 'docker:dind', value: '' }
], dependsOn: { key: 'enableBuild', value: true },
tip: '仅当需要 DinD / docker socket 时配置,留空则禁用' },
{ key: 'buildScript', label: 'script', type: 'code', language: 'shell',
default:
'echo "📦 building $CI_PROJECT_NAME"\n# 添加 Dockerfile / 构建脚本\ndocker login -u "$REGISTRY_USER" -p "$REGISTRY_PASS" "$CI_REGISTRY"\ndocker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .\ndocker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"',
dependsOn: { key: 'enableBuild', value: true } },
{ key: 'buildArtifacts', label: 'artifacts.paths', type: 'text',
default: 'dist/\nbuild/',
dependsOn: { key: 'enableBuild', value: true }, tip: '多个路径用换行分隔' },
{ key: 'buildArtifactsExpire', label: 'artifacts.expire_in', type: 'text',
default: '1 week', dependsOn: { key: 'enableBuild', value: true } },
],
},
{
title: 'Job · test (自动化测试)',
fields: [
{ key: 'enableTest', label: '启用 test 阶段', type: 'switch', default: true },
{ key: 'testName', label: 'Job 名', type: 'text', default: 'test',
dependsOn: { key: 'enableTest', value: true } },
{ key: 'testImage', label: '镜像', type: 'text',
default: 'python:3.12-slim',
dependsOn: { key: 'enableTest', value: true } },
{ key: 'testStage', label: 'Stage', type: 'text', default: 'test',
dependsOn: { key: 'enableTest', value: true } },
{ key: 'testParallel', label: 'parallel (矩阵)', type: 'number',
min: 1, max: 50, default: 1, dependsOn: { key: 'enableTest', value: true } },
{ key: 'testCoverage', label: 'coverage 输入正则', type: 'text',
default: '', dependsOn: { key: 'enableTest', value: true },
tip: '如 /\\d+\\.\\d+%/,空则不上报 coverage' },
{ key: 'testScript', label: 'script', type: 'code', language: 'shell',
default:
'echo "🧪 running tests"\n# pytest --junitxml=report.xml --cov=src --cov-report=term\n# 收集 junit 报告供 GitLab UI 展示\n',
dependsOn: { key: 'enableTest', value: true } },
{ key: 'testArtifacts', label: 'artifacts.when/paths', type: 'text',
default: 'report.xml', dependsOn: { key: 'enableTest', value: true },
tip: '仅当有 junit/codecov 产出时填写' },
],
},
// ─────────────── Deploy 阶段(多环境)───────────────
{
title: 'Job · deploy (多环境)',
fields: [
{ key: 'enableDeploy', label: '启用 deploy 阶段', type: 'switch', default: true },
{ key: 'deployImage', label: '镜像', type: 'text',
default: 'alpine:3.20',
dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployStage', label: 'Stage', type: 'text', default: 'deploy',
dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployEnvironment', label: 'Deploy 环境 (Environment)', type: 'text',
default: 'production',
dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployNeeds', label: 'needs (上游 job)', type: 'text',
default: 'build', dependsOn: { key: 'enableDeploy', value: true },
tip: '逗号分隔多个 job 名' },
{ key: 'deployScript', label: 'script', type: 'code', language: 'shell',
default: 'echo "🚀 deploying $CI_PROJECT_NAME"\n# kubectl --context=prod apply -f k8s/\n# 或 ssh 推送 / helm upgrade --install',
dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployWhen', label: 'rules.only/except 分支', type: 'text',
default: 'main', dependsOn: { key: 'enableDeploy', value: true },
tip: '默认仅 main 部署;输入 main 或 main,master 支持多分支' },
{ key: 'deployWhenMode', label: '匹配模式', type: 'select', default: 'branches', options: [
{ label: 'branches (指定分支)', value: 'branches' },
{ label: 'tags (打 tag 后)', value: 'tags' },
{ label: 'merge_requests (MR)', value: 'merge_requests' },
{ label: 'schedules (定时)', value: 'schedules' },
{ label: 'web (Web 触发)', value: 'web' },
], dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployManual', label: 'manual gate (人工点击部署)', type: 'switch',
default: true, dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployAllowFailure', label: 'allow_failure (失败也继续)', type: 'switch',
default: false, dependsOn: { key: 'enableDeploy', value: true } },
],
},
// ─────────────── 缓存 ───────────────
{
title: '缓存与依赖',
fields: [
{ key: 'cacheKeyPrefix', label: 'cache.key 前缀', type: 'text',
default: '$CI_COMMIT_REF_SLUG',
tip: '留空则不输出 cache 块' },
{ key: 'cachePaths', label: 'cache.paths', type: 'text',
default: 'node_modules/\n.virtualenvs/\n.go/pkg/mod/\n.gradle/caches/\n.cache/',
tip: '一行一个路径,留空不输出 cache 块' },
{ key: 'cachePolicy', label: 'cache.policy', type: 'select', default: 'pull-push', options: [
{ label: 'pull-push (上传下载)', value: 'pull-push' },
{ label: 'pull (仅下载)', value: 'pull' },
{ label: 'push (仅上传)', value: 'push' },
] },
],
},
// ─────────────── Runner Tag ───────────────
{
title: 'Runner Tagsrunner 选择)',
fields: [
{ key: 'defaultTags', label: '默认 tags (在 default 段输出)',
type: 'text', default: '', tip: '逗号分隔;空则不输出 default.tags' },
],
},
],
}
// =====================================================================
// YAML 渲染(GitLab CI 是 YAML,和 toYaml 共用修正后版本)
// 数组里嵌对象要保证 list 形态,不能 dict 化
// =====================================================================
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)) {
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(`${baseIndent} - ${yamlScalar(item)}`)
}
}
} else if (typeof value === 'object') {
if (isEmptyMapping(value)) {
lines.push(`${baseIndent}${key}: {}`)
continue
}
lines.push(`${baseIndent}${key}:`)
const childIndent = baseIndent + ' '
lines.push(toYaml(value, childIndent))
} else {
lines.push(`${baseIndent}${key}: ${yamlScalar(value)}`)
}
}
return lines.join('\n')
}
// 把 keyvalue 形 {key:'name',value:'1'} 数组转成 {name:1,...} 简单映射;或保留 array
function toArrayOfKeyedObjects(arr) {
if (!Array.isArray(arr)) return []
// 形如 [{key:'docker:dind', value:''}] 在 services 中需要 {name:..., alias:...}
// GitLab services 形如: services: [docker:dind, postgres:15]
// 这里简单返回原值(去 value 为空的保留 key)
return arr
.filter(x => x && x.key && x.key !== '')
.map(x => x.key)
}
// ─────────────── 数据装配 ───────────────
function buildJob(cfg, name, stage, image, script, extra = {}) {
const job = {}
if (stage) job.stage = stage
if (image) job.image = image
if (Array.isArray(script) && script.length) job.script = script
Object.assign(job, extra)
// 过滤掉空
for (const k of Object.keys(job)) {
const v = job[k]
if (v === null || v === undefined || v === '' ||
(Array.isArray(v) && !v.length)) delete job[k]
}
return { [name]: job }
}
function buildStageList(stagesEnabled, customStages) {
if (!stagesEnabled) return null
const cleaned = (customStages || '')
.split(/[,\n]/)
.map(s => s.trim())
.filter(Boolean)
return cleaned.length ? cleaned : ['lint', 'build', 'test', 'deploy']
}
// 把多行文本变成 { script: [...] },保留空行当 echo
function splitScript(raw) {
if (typeof raw !== 'string') return []
return raw
.split('\n')
.map(s => s.trim())
.filter(s => s.length > 0)
}
export function generateGitlabCi(config) {
const doc = {}
// stages
const stages = buildStageList(config.stagesEnabled, config.stages)
if (stages) doc.stages = stages
// variables
const varsArr = []
if (config.variablesMode && Array.isArray(config.variablesMode)) {
for (const kv of config.variablesMode) {
if (kv && kv.key && kv.key !== '') varsArr.push({ key: kv.key, value: String(kv.value ?? '') })
}
}
if (varsArr.length) doc.variables = Object.fromEntries(varsArr.map(v => [v.key, v.value]))
// default
const defaultObj = {}
if (config.defaultImage) defaultObj.image = config.defaultImage
if (config.defaultTags) defaultObj.tags = config.defaultTags.split(',').map(t => t.trim()).filter(Boolean)
// cache in default
if (config.cacheKeyPrefix && config.cachePaths) {
const paths = config.cachePaths.split('\n').map(s => s.trim()).filter(Boolean)
if (paths.length) {
defaultObj.cache = {
key: { files: [config.cacheKeyPrefix] },
paths,
policy: config.cachePolicy || 'pull-push',
}
}
}
if (Object.keys(defaultObj).length) doc.default = defaultObj
// workflow
if (config.workflowPolicy && config.workflowPolicy !== 'auto') {
if (config.workflowPolicy === 'none') {
doc.workflow = { rules: [{ when: 'never' }] }
} else {
const w = { rules: [{ when: config.workflowPolicy }] }
if (config.workflowInputs) {
try {
const parsed = yaml.safeLoad ? null : null // noop; we keep simple
} catch (e) { /* ignore */ }
}
doc.workflow = w
}
}
// include (高级用户自填)
if (config.includeMode && config.includeMode.trim()) {
// 解析:用户填的是合法 YAML 片段;这里做最稳:尝试 as 字符串
const incRaw = config.includeMode.trim()
// 简单判定:用 include 直接给字符串或对象数组
try {
// 用内置简易辨识:包含 ":" 视为子对象
const lines = incRaw.split('\n').map(l => l).filter(Boolean)
const looksLikeMap = lines.length <= 1 && lines[0].startsWith('-')
if (looksLikeMap) {
doc.include = '<<<请把 include 写成多行 YAML 块>>>'
} else {
doc.include = incRaw
}
} catch (e) {
doc.include = incRaw
}
}
// lint job
if (config.enableLint) {
Object.assign(doc, buildJob(
config, config.lintName || 'lint', config.lintStage || 'lint',
config.lintImage, splitScript(config.lintScript),
{ allow_failure: !!config.lintAllowFailure }
))
}
// build job
if (config.enableBuild) {
const services = toArrayOfKeyedObjects(config.buildServices)
Object.assign(doc, buildJob(
config, config.buildName || 'build', config.buildStage || 'build',
config.buildImage, splitScript(config.buildScript),
{
services: services.length ? services : undefined,
artifacts: (() => {
const paths = (config.buildArtifacts || '').split('\n').map(s => s.trim()).filter(Boolean)
if (!paths.length) return undefined
return { paths, expire_in: config.buildArtifactsExpire || '1 week' }
})(),
}
))
}
// test job
if (config.enableTest) {
const extra = {}
if (config.testCoverage) {
extra.coverage = config.testCoverage
}
if (config.testArtifacts) {
extra.artifacts = {
when: 'always',
reports: { junit: config.testArtifacts },
}
}
Object.assign(doc, buildJob(
config, config.testName || 'test', config.testStage || 'test',
config.testImage, splitScript(config.testScript), extra
))
// parallel(matrix)
if (config.testParallel && Number(config.testParallel) > 1) {
const jobName = config.testName || 'test'
doc[jobName].parallel = Number(config.testParallel)
}
}
// deploy job
if (config.enableDeploy) {
const extras = {
environment: { name: config.deployEnvironment || 'production' },
needs: (config.deployNeeds || '').split(',').map(s => s.trim()).filter(Boolean),
when: config.deployManual ? 'manual' : 'on_success',
allow_failure: !!config.deployAllowFailure,
}
const rulesList = (config.deployWhen || '')
.split(/[,\n]/)
.map(s => s.trim())
.filter(Boolean)
if (rulesList.length) {
extras.rules = [{
if: [`$CI_COMMIT_${(config.deployWhenMode || 'branches').toUpperCase()} =~ /^(${rulesList.join('|')})$/`],
}]
}
Object.assign(doc, buildJob(
config, 'deploy', config.deployStage || 'deploy',
config.deployImage, splitScript(config.deployScript), extras
))
}
const header = `# .gitlab-ci.yml — 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 配套 GitLab Runner: 自托管或共享 Runner (tag 匹配)\n# 用法: 提交此文件至仓库根目录即可触发流水线\n\n`
return header + toYaml(doc)
}
+7 -1
View File
@@ -77,7 +77,9 @@ import { ansiblePlaybookSchema, generateAnsiblePlaybookYaml } from './ansible-pl
// ============ CI/CD ============
import { gitlabSchema, generateGitlabRb } from './gitlab'
import { gitlabCiSchema, generateGitlabCi } from './gitlab-ci'
import { jenkinsSchema, generateJenkinsYaml } from './jenkins'
import { jenkinsPipelineSchema, generateJenkinsfile } from './jenkins-pipeline'
// ============ 高可用 ============
import { keepalivedSchema, generateKeepalivedConf } from './keepalived'
@@ -171,7 +173,9 @@ export const schemas = {
'ansible-playbook': ansiblePlaybookSchema,
// CI/CD
gitlab: gitlabSchema,
'gitlab-ci': gitlabCiSchema,
jenkins: jenkinsSchema,
'jenkins-pipeline': jenkinsPipelineSchema,
// 高可用
keepalived: keepalivedSchema,
haproxy: haproxySchema,
@@ -246,7 +250,9 @@ export const generators = {
'ansible-playbook': generateAnsiblePlaybookYaml,
// CI/CD
gitlab: generateGitlabRb,
'gitlab-ci': generateGitlabCi,
jenkins: generateJenkinsYaml,
'jenkins-pipeline': generateJenkinsfile,
// 高可用
keepalived: generateKeepalivedConf,
haproxy: generateHaproxyCfg,
@@ -357,7 +363,7 @@ export const categories = [
{
name: 'CI/CD',
icon: 'Files',
items: ['gitlab', 'jenkins'],
items: ['gitlab', 'gitlab-ci', 'jenkins', 'jenkins-pipeline'],
},
{
name: '高可用',
+399
View File
@@ -0,0 +1,399 @@
// Jenkins 声明式流水线 (Jenkinsfile) 生成器 for ConfTemplate
// 生成符合 Jenkins Declarative Pipeline 2.x 规范的 Groovy DSL
export const jenkinsPipelineSchema = {
id: 'jenkins-pipeline',
name: 'Jenkinsfile',
icon: 'Cpu',
category: 'CI/CD',
description: '生成 Jenkins 声明式流水线 (Declarative Pipeline)',
format: 'groovy',
fileName: 'Jenkinsfile',
groups: [
// ─────────────── Agent ───────────────
{
title: '执行 Agent',
fields: [
{ key: 'agentMode', label: 'agent 模式', type: 'select', default: 'docker', options: [
{ label: 'Docker 容器 (推荐)', value: 'docker' },
{ label: '任意可用节点', value: 'any' },
{ label: '指定 label 节点', value: 'label' },
{ label: '无 agent (在顶层 controller 跑)', value: 'none' },
{ label: 'Kubernetes Pod', value: 'kubernetes' },
] },
{ key: 'agentLabel', label: 'agent label',
type: 'text', default: 'linux',
dependsOn: { key: 'agentMode', value: 'label' } },
{ key: 'agentImage', label: 'Docker 镜像',
type: 'text', default: 'maven:3.9.6-eclipse-temurin-17',
dependsOn: { key: 'agentMode', value: 'docker' } },
{ key: 'agentDockerArgs', label: 'args (docker run 参数)',
type: 'text', default: '-v $HOME:/root -v /var/run/docker.sock:/var/run/docker.sock',
dependsOn: { key: 'agentMode', value: 'docker' },
tip: '需要 DinD / 宿主 docker / 缓存挂载时填' },
{ key: 'agentK8sYAML', label: 'kubernetes YAML',
type: 'code', language: 'yaml', default:
'apiVersion: v1\nkind: Pod\nspec:\n containers:\n - name: jnlp\n image: jenkins/inbound-agent:3293.vd85995_5c7116-1\n',
dependsOn: { key: 'agentMode', value: 'kubernetes' } },
],
},
// ─────────────── Tools ───────────────
{
title: 'Tools (自动安装)',
fields: [
{ key: 'enableTools', label: 'tools 段', type: 'switch', default: false,
tip: '需要 Jenkins 配置好了 Maven / JDK / Gradle 等安装时启用' },
{ key: 'toolMaven', label: 'maven 安装名', type: 'text',
default: 'maven-3.9', dependsOn: { key: 'enableTools', value: true } },
{ key: 'toolJdk', label: 'jdk 安装名', type: 'text',
default: 'jdk-17', dependsOn: { key: 'enableTools', value: true } },
{ key: 'toolGradle', label: 'gradle 安装名', type: 'text',
default: '', dependsOn: { key: 'enableTools', value: true } },
{ key: 'toolNodejs', label: 'nodejs 安装名', type: 'text',
default: '', dependsOn: { key: 'enableTools', value: true } },
],
},
// ─────────────── Environment ───────────────
{
title: '环境变量',
fields: [
{ key: 'envVars', label: 'environment env', type: 'keyvalue', default: [
{ key: 'PROJECT_NAME', value: 'my-app' },
{ key: 'DOCKER_REGISTRY', value: 'registry.example.com' },
], tip: 'Jenkinsfile 顶层 environment 段,Pipeline Agent 与所有 stages 都能用' },
],
},
// ─────────────── Options ───────────────
{
title: 'options (全局行为)',
fields: [
{ key: 'optDisableConcurrent', label: 'disableConcurrentBuilds',
type: 'switch', default: true, tip: '禁止并发构建同一项目' },
{ key: 'optTimeoutMinutes', label: 'timeout', type: 'number',
min: 0, max: 1440, default: 60, tip: '0 = 不设置超时' },
{ key: 'optTimestamps', label: 'timestamps', type: 'switch', default: true },
{ key: 'optAnsiColor', label: 'ansiColor (彩色日志)',
type: 'switch', default: true },
{ key: 'optBuildDiscarderDays', label: 'buildDiscarder 保留天数',
type: 'number', min: 0, max: 365, default: 30, tip: '0 = 不设置' },
{ key: 'optSkipDefaultCheckout', label: 'skipDefaultCheckout',
type: 'switch', default: false },
],
},
// ─────────────── Triggers ───────────────
{
title: 'Triggers (触发器)',
fields: [
{ key: 'triggerWebhook', label: 'GitHub webhook (trigger { githubPush() })',
type: 'switch', default: true },
{ key: 'triggerPollSCM', label: 'pollSCM 定时轮询', type: 'switch', default: false },
{ key: 'triggerPollCron', label: 'pollSCM cron', type: 'text',
default: 'H/15 * * * *', dependsOn: { key: 'triggerPollSCM', value: true } },
{ key: 'triggerCron', label: 'cron 定时构建', type: 'text',
default: '', tip: 'H H(0-7) * * * ;空则不启用 cron 触发' },
],
},
// ─────────────── Stages ───────────────
{
title: 'Stage · Checkout',
fields: [
{ key: 'enableCheckout', label: '启用 Checkout stage', type: 'switch', default: true },
{ key: 'checkoutScript', label: 'steps 脚本', type: 'code',
language: 'groovy', default:
'checkout scmGit(\n branches: [[name: env.BRANCH_NAME ?: \'*/main\']],\n userRemoteConfigs: [[url: env.GIT_URL ?: \'https://example.com/group/repo.git\']]\n)',
dependsOn: { key: 'enableCheckout', value: true },
tip: 'scmGit() 是 Pipeline: SCM Step 3.x 推荐写法' },
],
},
{
title: 'Stage · Lint',
fields: [
{ key: 'enableLint', label: '启用 Lint stage', type: 'switch', default: true },
{ key: 'lintSteps', label: 'steps', type: 'code', language: 'groovy',
default: '// 静态检查:eslint / ruff / shellcheck / hadolint\nsh \'echo "🔍 running lint"\'\n// sh \'make lint\'',
dependsOn: { key: 'enableLint', value: true } },
],
},
{
title: 'Stage · Build',
fields: [
{ key: 'enableBuild', label: '启用 Build stage', type: 'switch', default: true },
{ key: 'buildSteps', label: 'steps', type: 'code', language: 'groovy',
default: '// 多语言示例:Maven / Gradle / npm / docker build\nsh \'\'\'\n echo "📦 building ${env.PROJECT_NAME}"\n mvn -B clean package -DskipTests\n\'\'\'',
dependsOn: { key: 'enableBuild', value: true } },
{ key: 'buildPostArchive', label: 'archiveArtifacts', type: 'switch',
default: true, dependsOn: { key: 'enableBuild', value: true } },
{ key: 'buildArtifactsGlob', label: 'artifacts glob 路径',
type: 'text', default: 'target/**/*.jar\ndist/**',
dependsOn: { key: 'enableBuild', value: true },
tip: '多个 glob 一行一个' },
],
},
{
title: 'Stage · Test',
fields: [
{ key: 'enableTest', label: '启用 Test stage', type: 'switch', default: true },
{ key: 'testSteps', label: 'steps', type: 'code', language: 'groovy',
default: 'sh \'mvn test\'\n// 也可用 junit + jacoco 步骤\n// junit \'target/surefire-reports/*.xml\'',
dependsOn: { key: 'enableTest', value: true } },
{ key: 'testPostJunit', label: 'junit 报表目录', type: 'text',
default: 'target/surefire-reports/*.xml',
dependsOn: { key: 'enableTest', value: true },
tip: '空则不输出 junit 步骤' },
],
},
{
title: 'Stage · Deploy',
fields: [
{ key: 'enableDeploy', label: '启用 Deploy stage', type: 'switch', default: true },
{ key: 'deployWhenBranch', label: '仅当 BRANCH_NAME 为', type: 'text',
default: 'main',
dependsOn: { key: 'enableDeploy', value: true },
tip: '空则不添加 when 条件;多分支用逗号' },
{ key: 'deploySteps', label: 'steps', type: 'code', language: 'groovy',
default: 'echo "🚀 deploying ${env.PROJECT_NAME}"\n// sh \'kubectl --context=prod apply -f k8s/\'\n// 或 sshPublisher / rancher / helm',
dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployInput', label: 'input (人工确认才执行)',
type: 'switch', default: true,
dependsOn: { key: 'enableDeploy', value: true } },
{ key: 'deployInputMessage', label: 'input.message',
type: 'text', default: '确认发布到生产环境?',
dependsOn: { key: 'enableDeploy', value: true } },
],
},
// ─────────────── Post ───────────────
{
title: 'post 段 (行为钩子)',
fields: [
{ key: 'postAlways', label: 'always', type: 'code', language: 'groovy',
default: 'echo \'always runs\'' },
{ key: 'postSuccess', label: 'success', type: 'code', language: 'groovy',
default: 'echo \'✅ deployed successfully\'' },
{ key: 'postFailure', label: 'failure', type: 'code', language: 'groovy',
default: 'echo \'❌ failed\'\n// mail to: \'devops@example.com\', subject: "Build Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"' },
{ key: 'postUnstable', label: 'unstable', type: 'code', language: 'groovy',
default: 'echo \'⚠️ unstable\'' },
{ key: 'postCleanup', label: 'cleanup', type: 'code', language: 'groovy',
default: '// 删除工作空间文件\n// cleanWs()' },
],
},
],
}
// =====================================================================
// Groovy / Jenkinsfile 渲染器(专门为流水线 DSL 而非通用 Groovy
// =====================================================================
function indent(s, n) {
if (!s) return ''
const pad = ' '.repeat(n)
return s.split('\n').map(l => l.trim() === '' ? '' : pad + l).join('\n')
}
function escSteps(raw) {
// 用户多行 steps; 但当 steps 里需要 Groovy 多行字符串时为模板留 %% 我为括号的占位
return raw.trim()
}
export function generateJenkinsfile(config) {
const lines = []
lines.push('// Jenkinsfile — 由 ConfTemplate 生成')
lines.push('// 生成时间: ' + new Date().toLocaleString('zh-CN'))
lines.push('// 用法: 放到仓库根目录, Jenkins Job 选 "Pipeline from SCM"')
lines.push('')
lines.push('pipeline {')
// ============ agent ============
lines.push(' agent {')
if (config.agentMode === 'none') {
lines.push(' none')
} else if (config.agentMode === 'any') {
lines.push(' any')
} else if (config.agentMode === 'label') {
lines.push(" label '" + (config.agentLabel || 'linux') + "'")
} else if (config.agentMode === 'docker') {
lines.push(' docker {')
lines.push(" image '" + (config.agentImage || 'maven:3.9-eclipse-temurin-17') + "'")
if (config.agentDockerArgs && config.agentDockerArgs.trim()) {
// 用户的 args 可能含 -v $HOME:/root (含 $ 符号,单引号内 $HOME 在 Groovy 是字面量;要双引号则会被 Groovy 插值,把 $HOME 写为 ${HOME}
const a = config.agentDockerArgs.trim()
// 用三引号避免任何内部冲突
lines.push(" args '''" + a + "'''")
}
lines.push(' reuseNode true')
lines.push(' }')
} else if (config.agentMode === 'kubernetes') {
// 多行 YAML,用三引号嵌入
const yamlBlock = (config.agentK8sYAML || '').trim()
lines.push(' kubernetes {')
lines.push(" yaml '''" + yamlBlock + "'''")
lines.push(' }')
}
lines.push(' }')
// ============ tools ============
if (config.enableTools) {
lines.push(' tools {')
if (config.toolMaven) lines.push(" maven '" + config.toolMaven + "'")
if (config.toolJdk) lines.push(" jdk '" + config.toolJdk + "'")
if (config.toolGradle) lines.push(" gradle '" + config.toolGradle + "'")
if (config.toolNodejs) lines.push(" nodejs '" + config.toolNodejs + "'")
lines.push(' }')
}
// ============ environment ============
const envArr = (config.envVars || []).filter(kv => kv && kv.key)
if (envArr.length) {
lines.push(' environment {')
for (const kv of envArr) {
lines.push(" " + kv.key + " = '" + String(kv.value ?? '').replace(/'/g, "\\'") + "'")
}
lines.push(' }')
}
// ============ options ============
const opts = []
if (config.optDisableConcurrent) opts.push(" disableConcurrentBuilds()")
if (config.optTimeoutMinutes && Number(config.optTimeoutMinutes) > 0) {
opts.push(" timeout(time: " + Number(config.optTimeoutMinutes) + ", unit: 'MINUTES')")
}
if (config.optTimestamps) opts.push(" timestamps()")
if (config.optAnsiColor) opts.push(" ansiColor('xterm')")
if (config.optBuildDiscarderDays && Number(config.optBuildDiscarderDays) > 0) {
opts.push(" buildDiscarder(logRotator(numToKeepStr: '" + config.optBuildDiscarderDays + "'))")
}
if (config.optSkipDefaultCheckout) opts.push(" skipDefaultCheckout()")
if (opts.length) {
lines.push(' options {')
for (const o of opts) lines.push(' ' + o.trimStart())
lines.push(' }')
}
// ============ triggers ============
const triggers = []
if (config.triggerWebhook) triggers.push("githubPush()")
if (config.triggerCron) triggers.push("cron('" + config.triggerCron + "')")
if (config.triggerPollSCM && config.triggerPollCron)
triggers.push("pollSCM('" + config.triggerPollCron + "')")
if (triggers.length) {
lines.push(' triggers {')
for (const t of triggers) lines.push(' ' + t.trimStart())
lines.push(' }')
}
// ============ stages ============
lines.push(' stages {')
// Helper: 生成 stage(name) { steps { ... } }optionally 加 when、input、post
function addStage(name, opts) {
lines.push(" stage('" + name + "') {")
if (opts.when) {
lines.push(' when {')
for (const w of opts.when) lines.push(w)
lines.push(' }')
}
// steps 块
lines.push(' steps {')
if (opts.input) {
lines.push(" input {")
lines.push(" message '" + (opts.inputMessage || '确认?') + "'")
lines.push(" ok '确定'")
lines.push(" submitter 'admin'")
lines.push(" }")
}
if (opts.steps) lines.push(indent(opts.steps, 8))
lines.push(' }')
// stage 级别 post(独立于 steps),注意缩进跟 stage steps 同级
if (opts.post) {
lines.push(' post {')
for (const p of opts.post) {
lines.push(" " + p.cond + " {")
if (p.script) lines.push(indent(p.script, 10))
lines.push(' }')
}
lines.push(' }')
}
lines.push(' }')
}
// Checkout
if (config.enableCheckout) {
addStage('Checkout', {
steps: config.checkoutScript || "checkout scm",
})
}
// Lint
if (config.enableLint) {
addStage('Lint', {
steps: config.lintSteps || 'sh \'echo "lint"\'',
})
}
// Build
if (config.enableBuild) {
const post = []
if (config.buildPostArchive) {
const globs = (config.buildArtifactsGlob || '').split('\n').map(s => s.trim()).filter(Boolean)
if (globs.length) {
const arr = globs.map(g => "'" + g + "'").join(', ')
post.push({ cond: 'success', script: "archiveArtifacts artifacts: [" + arr + "], allowEmptyArchive: false" })
} else {
post.push({ cond: 'success', script: "// archiveArtifacts 配置为空,跳过" })
}
}
addStage('Build', {
steps: config.buildSteps || 'sh \'echo "build"\'',
post,
})
}
// Test
if (config.enableTest) {
const post = []
if (config.testPostJunit && config.testPostJunit.trim()) {
post.push({ cond: 'always', script: "junit '" + config.testPostJunit + "'" })
}
addStage('Test', {
steps: config.testSteps || 'sh \'echo "test"\'',
post,
})
}
// Deploy
if (config.enableDeploy) {
const when = []
if (config.deployWhenBranch && config.deployWhenBranch.trim()) {
const branches = config.deployWhenBranch.split(/[,\n]/).map(s => s.trim()).filter(Boolean)
if (branches.length) {
const re = '^(' + branches.join('|') + ')$'
when.push(" branch '" + re + "'")
}
}
addStage('Deploy', {
steps: config.deploySteps || 'sh \'echo "deploy"\'',
when: when.length ? when : null,
input: config.deployInput,
inputMessage: config.deployInputMessage,
})
}
lines.push(' }') // stages 结束
// ============ post (顶层) ============
const postSections = [
{ cond: 'always', body: config.postAlways },
{ cond: 'success', body: config.postSuccess },
{ cond: 'failure', body: config.postFailure },
{ cond: 'unstable', body: config.postUnstable },
{ cond: 'cleanup', body: config.postCleanup },
].filter(p => p.body && String(p.body).trim().length)
if (postSections.length) {
lines.push(' post {')
for (const p of postSections) {
lines.push(' ' + p.cond + ' {')
lines.push(indent(p.body, 6))
lines.push(' }')
}
lines.push(' }')
}
lines.push('}') // pipeline 结束
lines.push('')
return lines.join('\n')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+50 -18
View File
@@ -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')
+46 -23
View File
@@ -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')
+65 -23
View File
@@ -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')
+46 -17
View File
@@ -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')
}
+50 -18
View File
@@ -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')
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -23
View File
@@ -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')
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -17
View File
@@ -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')
}
+46 -23
View File
@@ -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')
+46 -17
View File
@@ -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')
}