diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 0e1c2ce..9df45f9 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -60,7 +60,7 @@ diff --git a/src/schemas/dockerfile.js b/src/schemas/dockerfile.js new file mode 100644 index 0000000..a0b565f --- /dev/null +++ b/src/schemas/dockerfile.js @@ -0,0 +1,364 @@ +export const dockerfileSchema = { + id: 'dockerfile', + name: 'Dockerfile', + icon: 'Box', + category: '容器化', + description: 'Docker 容器镜像构建文件', + format: 'dockerfile', + fileName: 'Dockerfile', + groups: [ + { + title: '基础镜像', + fields: [ + { key: 'baseImage', label: '基础镜像', type: 'text', placeholder: 'nginx:1.24-alpine', default: 'nginx:1.24-alpine', required: true, tip: 'FROM 指令' }, + { key: 'fromPlatform', label: '平台架构', type: 'select', options: [ + { label: '不限制', value: '' }, + { label: 'linux/amd64', value: 'linux/amd64' }, + { label: 'linux/arm64', value: 'linux/arm64' }, + ], default: '', tip: '多架构构建时指定' }, + { key: 'enableMultiStage', label: '多阶段构建', type: 'switch', default: false, tip: '分离构建和运行阶段' }, + { key: 'builderImage', label: '构建阶段镜像', type: 'text', placeholder: 'node:18-alpine', default: 'node:18-alpine', dependsOn: { key: 'enableMultiStage', value: true } }, + { key: 'builderAs', label: '构建阶段别名', type: 'text', default: 'builder', dependsOn: { key: 'enableMultiStage', value: true } }, + ], + }, + { + title: '元数据 (LABEL)', + fields: [ + { key: 'enableLabels', label: '添加标签', type: 'switch', default: false }, + { key: 'labelMaintainer', label: '维护者', type: 'text', placeholder: 'team@example.com', default: 'team@example.com', dependsOn: { key: 'enableLabels', value: true } }, + { key: 'labelVersion', label: '版本', type: 'text', placeholder: '1.0.0', default: '1.0.0', dependsOn: { key: 'enableLabels', value: true } }, + { key: 'labelDescription', label: '描述', type: 'text', placeholder: 'My application', default: '', dependsOn: { key: 'enableLabels', value: true } }, + ], + }, + { + title: '构建参数 (ARG)', + fields: [ + { key: 'enableArgs', label: '定义构建参数', type: 'switch', default: false, tip: '仅在 docker build --build-arg 时生效' }, + { key: 'args', label: '参数列表 (KEY=VALUE)', type: 'text', placeholder: 'APP_VERSION=1.0.0\nNODE_ENV=production', default: 'APP_VERSION=1.0.0', dependsOn: { key: 'enableArgs', value: true }, tip: '每行一个,格式 KEY 或 KEY=默认值' }, + ], + }, + { + title: '环境变量 (ENV)', + fields: [ + { key: 'enableEnv', label: '设置环境变量', type: 'switch', default: false }, + { key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'NODE_ENV=production\nAPP_PORT=8080', default: 'NODE_ENV=production\nAPP_PORT=8080', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' }, + ], + }, + { + title: '工作目录与用户', + fields: [ + { key: 'enableWorkdir', label: '设置工作目录', type: 'switch', default: true }, + { key: 'workdir', label: '工作目录', type: 'text', default: '/app', dependsOn: { key: 'enableWorkdir', value: true } }, + { key: 'enableUser', label: '切换运行用户', type: 'switch', default: false, tip: '生产环境建议使用非 root 用户' }, + { key: 'userName', label: '用户名或 UID', type: 'text', placeholder: '1001:1001', default: '1001:1001', dependsOn: { key: 'enableUser', value: true } }, + { key: 'enableUseradd', label: '创建用户', type: 'switch', default: false, dependsOn: { key: 'enableUser', value: true }, tip: '在 RUN 中创建用户' }, + { key: 'createUserName', label: '用户名', type: 'text', default: 'appuser', dependsOn: { key: 'enableUseradd', value: true } }, + ], + }, + { + title: '文件复制', + fields: [ + { key: 'enableCopy', label: '复制文件', type: 'switch', default: true }, + { key: 'copySrc', label: '源路径', type: 'text', placeholder: '. 或 ./dist', default: '.', dependsOn: { key: 'enableCopy', value: true } }, + { key: 'copyDest', label: '目标路径', type: 'text', placeholder: '/app', default: '/app', dependsOn: { key: 'enableCopy', value: true } }, + { key: 'enableCopyChown', label: '设置文件所有者', type: 'switch', default: false, dependsOn: { key: 'enableCopy', value: true } }, + { key: 'copyChown', label: '所有者', type: 'text', placeholder: '1001:1001', default: '1001:1001', dependsOn: { key: 'enableCopyChown', value: true } }, + { key: 'enableCopyChmod', label: '设置文件权限', type: 'switch', default: false, dependsOn: { key: 'enableCopy', value: true } }, + { key: 'copyChmod', label: '权限', type: 'text', placeholder: '755', default: '755', dependsOn: { key: 'enableCopyChmod', value: true } }, + { key: 'enableAdd', label: '使用 ADD (自动解压/URL)', type: 'switch', default: false, tip: '需要解压 tar 或下载远程文件时使用' }, + { key: 'addSrc', label: 'ADD 源', type: 'text', placeholder: 'https://example.com/app.tar.gz', default: '', dependsOn: { key: 'enableAdd', value: true } }, + { key: 'addDest', label: 'ADD 目标', type: 'text', default: '/app/', dependsOn: { key: 'enableAdd', value: true } }, + ], + }, + { + title: '依赖安装 (RUN)', + fields: [ + { key: 'packageManager', label: '包管理器', type: 'select', options: [ + { label: '不安装系统包', value: 'none' }, + { label: 'apk (Alpine)', value: 'apk' }, + { label: 'apt (Debian/Ubuntu)', value: 'apt' }, + { label: 'yum (CentOS/RHEL)', value: 'yum' }, + { label: 'dnf (Fedora)', value: 'dnf' }, + ], default: 'none' }, + { key: 'systemPackages', label: '系统包 (空格分隔)', type: 'text', placeholder: 'curl wget tzdata', default: 'curl ca-certificates tzdata', dependsOn: { key: 'packageManager', valueNotIn: ['none'] } }, + { key: 'enableAppInstall', label: '安装应用依赖', type: 'switch', default: false, tip: 'npm install / pip install / go build 等' }, + { key: 'appInstallCmd', label: '安装命令', type: 'text', placeholder: 'npm ci --only=production', default: 'npm ci --only=production', dependsOn: { key: 'enableAppInstall', value: true } }, + { key: 'enableBuildCmd', label: '构建命令', type: 'switch', default: false, tip: 'npm run build / go build / mvn package 等' }, + { key: 'buildCmd', label: '构建命令', type: 'text', placeholder: 'npm run build', default: 'npm run build', dependsOn: { key: 'enableBuildCmd', value: true } }, + { key: 'enableCleanup', label: '清理缓存', type: 'switch', default: true, dependsOn: { key: 'packageManager', valueNotIn: ['none'] }, tip: '减小镜像体积' }, + ], + }, + { + title: '端口与网络', + fields: [ + { key: 'enableExpose', label: '暴露端口', type: 'switch', default: true }, + { key: 'exposePorts', label: '端口列表', type: 'text', placeholder: '80 443', default: '80', dependsOn: { key: 'enableExpose', value: true }, tip: '空格分隔多个端口' }, + ], + }, + { + title: '健康检查 (HEALTHCHECK)', + fields: [ + { key: 'enableHealthcheck', label: '添加健康检查', type: 'switch', default: false }, + { key: 'healthcheckType', label: '检查方式', type: 'select', options: [ + { label: 'CMD (自定义命令)', value: 'CMD' }, + { label: 'NONE (禁用继承的健康检查)', value: 'NONE' }, + ], default: 'CMD', dependsOn: { key: 'enableHealthcheck', value: true } }, + { key: 'healthcheckInterval', label: '检查间隔', type: 'text', default: '30s', dependsOn: { key: 'healthcheckType', value: 'CMD' } }, + { key: 'healthcheckTimeout', label: '超时时间', type: 'text', default: '10s', dependsOn: { key: 'healthcheckType', value: 'CMD' } }, + { key: 'healthcheckRetries', label: '重试次数', type: 'number', min: 1, max: 10, default: 3, dependsOn: { key: 'healthcheckType', value: 'CMD' } }, + { key: 'healthcheckStartPeriod', label: '启动等待期', type: 'text', default: '5s', dependsOn: { key: 'healthcheckType', value: 'CMD' } }, + { key: 'healthcheckCmd', label: '检查命令', type: 'text', placeholder: 'curl -f http://localhost/ || exit 1', default: 'curl -f http://localhost:80/ || exit 1', dependsOn: { key: 'healthcheckType', value: 'CMD' }, required: true }, + ], + }, + { + title: '挂载点与数据卷', + fields: [ + { key: 'enableVolume', label: '声明数据卷', type: 'switch', default: false }, + { key: 'volumePaths', label: '挂载路径', type: 'text', placeholder: '/data /var/log/app', default: '/data', dependsOn: { key: 'enableVolume', value: true }, tip: '空格分隔多个路径' }, + ], + }, + { + title: '入口与启动', + fields: [ + { key: 'enableEntrypoint', label: '设置 ENTRYPOINT', type: 'switch', default: false, tip: '容器启动时执行的固定命令' }, + { key: 'entrypointCmd', label: 'ENTRYPOINT 命令', type: 'text', placeholder: '["java", "-jar", "app.jar"]', default: '["node", "server.js"]', dependsOn: { key: 'enableEntrypoint', value: true }, tip: 'JSON 数组格式或 shell 格式' }, + { key: 'enableCmd', label: '设置 CMD', type: 'switch', default: true, tip: '容器启动时的默认参数' }, + { key: 'cmdType', label: 'CMD 格式', type: 'select', options: [ + { label: 'exec 格式 (推荐)', value: 'exec' }, + { label: 'shell 格式', value: 'shell' }, + ], default: 'exec', dependsOn: { key: 'enableCmd', value: true } }, + { key: 'cmdValue', label: 'CMD 内容', type: 'text', placeholder: '["nginx", "-g", "daemon off;"]', default: '["nginx", "-g", "daemon off;"]', dependsOn: { key: 'enableCmd', value: true } }, + ], + }, + { + title: '信号与停止', + fields: [ + { key: 'enableStopSignal', label: '自定义停止信号', type: 'switch', default: false, tip: 'docker stop 发送的信号' }, + { key: 'stopSignal', label: '停止信号', type: 'select', options: [ + { label: 'SIGTERM (默认)', value: 'SIGTERM' }, + { label: 'SIGQUIT', value: 'SIGQUIT' }, + { label: 'SIGINT', value: 'SIGINT' }, + { label: 'SIGUSR1', value: 'SIGUSR1' }, + ], default: 'SIGTERM', dependsOn: { key: 'enableStopSignal', value: true } }, + { key: 'enableShell', label: '指定 Shell', type: 'switch', default: false, tip: 'SHELL 指令,RUN 使用的 shell' }, + { key: 'shellPath', label: 'Shell 路径', type: 'text', default: '/bin/sh', dependsOn: { key: 'enableShell', value: true } }, + { key: 'shellFlag', label: 'Shell 参数', type: 'text', default: '-c', dependsOn: { key: 'enableShell', value: true } }, + ], + }, + ], +} + +export function generateDockerfile(config) { + const lines = [] + lines.push(`# Dockerfile - 由 ConfTemplate 生成`) + lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`) + lines.push(``) + + // Builder stage + if (config.enableMultiStage) { + lines.push(`# ======================== 构建阶段 ========================`) + lines.push(`FROM${config.fromPlatform ? ` --platform=${config.fromPlatform}` : ''} ${config.builderImage} AS ${config.builderAs}`) + lines.push(``) + if (config.enableWorkdir) { + lines.push(`WORKDIR ${config.workdir}`) + } + lines.push(`COPY . .`) + if (config.enableAppInstall && config.appInstallCmd) { + lines.push(`RUN ${config.appInstallCmd}`) + } + if (config.enableBuildCmd && config.buildCmd) { + lines.push(`RUN ${config.buildCmd}`) + } + lines.push(``) + lines.push(`# ======================== 运行阶段 ========================`) + } + + // FROM + const platformArg = config.fromPlatform ? ` --platform=${config.fromPlatform}` : '' + if (config.enableMultiStage) { + lines.push(`FROM${platformArg} ${config.baseImage}`) + } else { + lines.push(`FROM${platformArg} ${config.baseImage}`) + } + lines.push(``) + + // ARG + if (config.enableArgs && config.args) { + config.args.split('\n').filter(Boolean).forEach(line => { + lines.push(`ARG ${line.trim()}`) + }) + lines.push(``) + } + + // LABEL + if (config.enableLabels) { + if (config.labelMaintainer) lines.push(`LABEL maintainer="${config.labelMaintainer}"`) + if (config.labelVersion) lines.push(`LABEL version="${config.labelVersion}"`) + if (config.labelDescription) lines.push(`LABEL description="${config.labelDescription}"`) + lines.push(``) + } + + // ENV + if (config.enableEnv && config.envVars) { + config.envVars.split('\n').filter(Boolean).forEach(line => { + lines.push(`ENV ${line.trim()}`) + }) + lines.push(``) + } + + // WORKDIR + if (config.enableWorkdir) { + lines.push(`WORKDIR ${config.workdir}`) + lines.push(``) + } + + // Package install + if (config.packageManager && config.packageManager !== 'none') { + const pkgs = (config.systemPackages || '').trim() + if (pkgs) { + if (config.packageManager === 'apk') { + lines.push(`RUN apk add --no-cache ${pkgs}`) + } else if (config.packageManager === 'apt') { + lines.push(`RUN apt-get update && \\`) + lines.push(` apt-get install -y --no-install-recommends ${pkgs} && \\`) + if (config.enableCleanup) { + lines.push(` rm -rf /var/lib/apt/lists/*`) + } + } else if (config.packageManager === 'yum') { + lines.push(`RUN yum install -y ${pkgs} && \\`) + if (config.enableCleanup) { + lines.push(` yum clean all && rm -rf /var/cache/yum`) + } + } else if (config.packageManager === 'dnf') { + lines.push(`RUN dnf install -y ${pkgs} && \\`) + if (config.enableCleanup) { + lines.push(` dnf clean all && rm -rf /var/cache/dnf`) + } + } + lines.push(``) + } + } + + // Create user + if (config.enableUser && config.enableUseradd && config.createUserName) { + lines.push(`RUN addgroup -S ${config.createUserName} && adduser -S ${config.createUserName} -G ${config.createUserName}`) + lines.push(``) + } + + // COPY from builder stage + if (config.enableMultiStage) { + if (config.enableCopyChown) { + lines.push(`COPY --from=${config.builderAs} --chown=${config.copyChown} ${config.workdir || '/app'} ${config.copyDest}`) + } else { + lines.push(`COPY --from=${config.builderAs} ${config.workdir || '/app'} ${config.copyDest}`) + } + lines.push(``) + } + + // COPY + if (config.enableCopy && !config.enableMultiStage) { + let copyFlags = [] + if (config.enableCopyChown && config.copyChown) copyFlags.push(`--chown=${config.copyChown}`) + if (config.enableCopyChmod && config.copyChmod) copyFlags.push(`--chmod=${config.copyChmod}`) + const flags = copyFlags.length ? ` ${copyFlags.join(' ')}` : '' + lines.push(`COPY${flags} ${config.copySrc} ${config.copyDest}`) + lines.push(``) + } + + // ADD + if (config.enableAdd && config.addSrc) { + lines.push(`ADD ${config.addSrc} ${config.addDest}`) + lines.push(``) + } + + // App install (non-multi-stage) + if (!config.enableMultiStage && config.enableAppInstall && config.appInstallCmd) { + lines.push(`RUN ${config.appInstallCmd}`) + lines.push(``) + } + + // Build cmd (non-multi-stage) + if (!config.enableMultiStage && config.enableBuildCmd && config.buildCmd) { + lines.push(`RUN ${config.buildCmd}`) + lines.push(``) + } + + // VOLUME + if (config.enableVolume && config.volumePaths) { + const paths = config.volumePaths.split(/\s+/).filter(Boolean) + if (paths.length === 1) { + lines.push(`VOLUME ["${paths[0]}"]`) + } else { + lines.push(`VOLUME [${paths.map(p => `"${p}"`).join(', ')}]`) + } + lines.push(``) + } + + // EXPOSE + if (config.enableExpose && config.exposePorts) { + lines.push(`EXPOSE ${config.exposePorts}`) + lines.push(``) + } + + // USER + if (config.enableUser) { + lines.push(`USER ${config.userName}`) + lines.push(``) + } + + // HEALTHCHECK + if (config.enableHealthcheck) { + if (config.healthcheckType === 'NONE') { + lines.push(`HEALTHCHECK NONE`) + } else { + let hc = `HEALTHCHECK` + if (config.healthcheckInterval) hc += ` --interval=${config.healthcheckInterval}` + if (config.healthcheckTimeout) hc += ` --timeout=${config.healthcheckTimeout}` + if (config.healthcheckRetries) hc += ` --retries=${config.healthcheckRetries}` + if (config.healthcheckStartPeriod) hc += ` --start-period=${config.healthcheckStartPeriod}` + hc += ` CMD ${config.healthcheckCmd}` + lines.push(hc) + } + lines.push(``) + } + + // STOPSIGNAL + if (config.enableStopSignal) { + lines.push(`STOPSIGNAL ${config.stopSignal}`) + lines.push(``) + } + + // SHELL + if (config.enableShell) { + lines.push(`SHELL ["${config.shellPath}", "${config.shellFlag}"]`) + lines.push(``) + } + + // ENTRYPOINT + if (config.enableEntrypoint && config.entrypointCmd) { + const val = config.entrypointCmd.trim() + if (val.startsWith('[')) { + lines.push(`ENTRYPOINT ${val}`) + } else { + lines.push(`ENTRYPOINT ["${val}"]`) + } + lines.push(``) + } + + // CMD + if (config.enableCmd && config.cmdValue) { + const val = config.cmdValue.trim() + if (config.cmdType === 'exec') { + if (val.startsWith('[')) { + lines.push(`CMD ${val}`) + } else { + lines.push(`CMD ["${val}"]`) + } + } else { + lines.push(`CMD ${val}`) + } + lines.push(``) + } + + return lines.join('\n') +} diff --git a/src/schemas/k8s-cronjob.js b/src/schemas/k8s-cronjob.js index 20953d5..90eabae 100644 --- a/src/schemas/k8s-cronjob.js +++ b/src/schemas/k8s-cronjob.js @@ -235,47 +235,87 @@ export const k8sCronJobSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) - for (const [key, value] of Object.entries(obj)) { +// ======================== 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] if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { + lines.push(prefix + key + ':') + for (var ai = 0; ai < value.length; ai++) { + var item = value[ai] if (typeof item === 'object' && item !== null) { - 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}`) - } - } + lines.push(renderArrayObject(item, indent + 1)) } else { - lines.push(`${prefix} - ${item}`) + lines.push(prefix + ' - ' + item) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) + lines.push(prefix + key + ':') lines.push(toYaml(value, indent + 1)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(prefix + key + ': ' + value) + } + } + return lines.join('\n') +} + +function renderArrayObject(item, indent) { + var prefix = '' + for (var p = 0; p < indent; p++) prefix += ' ' + var entries = Object.entries(item).filter(function(pair) { return pair[1] !== null && pair[1] !== undefined && pair[1] !== '' }) + if (entries.length === 0) return '' + var lines = [] + var first = entries[0] + var fk = first[0], fv = first[1] + if (Array.isArray(fv)) { + lines.push(prefix + '- ' + fk + ':') + for (var ai = 0; ai < fv.length; ai++) { + var arrItem = fv[ai] + if (typeof arrItem === 'object' && arrItem !== null) { + lines.push(renderArrayObject(arrItem, indent + 2)) + } else { + lines.push(prefix + ' - ' + arrItem) + } + } + } else if (typeof fv === 'object' && fv !== null) { + lines.push(prefix + '- ' + fk + ':') + lines.push(toYaml(fv, indent + 2)) + } else { + lines.push(prefix + '- ' + fk + ': ' + fv) + } + for (var i = 1; i < entries.length; i++) { + var k = entries[i][0], v = entries[i][1] + if (v === null || v === undefined || v === '') continue + if (Array.isArray(v)) { + lines.push(prefix + ' ' + k + ':') + for (var ai = 0; ai < v.length; ai++) { + var arrItem = v[ai] + if (typeof arrItem === 'object' && arrItem !== null) { + lines.push(renderArrayObject(arrItem, indent + 2)) + } else { + lines.push(prefix + ' - ' + arrItem) + } + } + } else if (typeof v === 'object') { + lines.push(prefix + ' ' + k + ':') + lines.push(toYaml(v, indent + 2)) + } else { + lines.push(prefix + ' ' + k + ': ' + v) } } return lines.join('\n') } export function generateK8sCronJobYaml(config) { - const container = { + var container = { name: config.name, image: config.image, imagePullPolicy: config.imagePullPolicy, @@ -299,9 +339,9 @@ export function generateK8sCronJobYaml(config) { // 环境变量 if (config.enableEnv && config.envVars) { - container.env = config.envVars.split('\n').filter(Boolean).map(line => { - const [k, ...v] = line.split('=') - return { name: k.trim(), value: v.join('=').trim() } + container.env = config.envVars.split('\n').filter(Boolean).map(function(line) { + var idx = line.indexOf('=') + return { name: line.substring(0, idx).trim(), value: line.substring(idx + 1).trim() } }) } if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) { @@ -311,11 +351,11 @@ export function generateK8sCronJobYaml(config) { } // 卷挂载 - const volumes = [] + var volumes = [] if (config.enableVolumeMount) { container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }] - const vol = { name: config.volumeName } - const vt = config.volumeType + var vol = { name: config.volumeName } + var vt = config.volumeType if (vt === 'emptyDir') vol.emptyDir = {} else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName } else if (vt === 'configMap') vol.configMap = { name: config.configMapName } @@ -335,7 +375,7 @@ export function generateK8sCronJobYaml(config) { } } - const podSpec = { + var podSpec = { containers: [container], restartPolicy: config.restartPolicy, } @@ -351,7 +391,7 @@ export function generateK8sCronJobYaml(config) { podSpec.securityContext = { fsGroup: config.fsGroup } } - const cronjob = { + var cronjob = { apiVersion: 'batch/v1', kind: 'CronJob', metadata: { @@ -377,7 +417,7 @@ export function generateK8sCronJobYaml(config) { }, } - const header = `# K8s CronJob - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'cronjob.yaml'}\n` + var header = '# K8s CronJob - 由 ConfTemplate 生成\n# 生成时间: ' + new Date().toLocaleString('zh-CN') + '\n# 部署命令: kubectl apply -f ' + (config.fileName || 'cronjob.yaml') + '\n' return header + '\n' + toYaml(cronjob) } diff --git a/src/schemas/k8s-job.js b/src/schemas/k8s-job.js index 541b5c6..e98ecd6 100644 --- a/src/schemas/k8s-job.js +++ b/src/schemas/k8s-job.js @@ -206,47 +206,87 @@ export const k8sJobSchema = { ], } -function toYaml(obj, indent = 0) { - const lines = [] - const prefix = ' '.repeat(indent) - for (const [key, value] of Object.entries(obj)) { +// ======================== 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] if (value === null || value === undefined || value === '') continue if (Array.isArray(value)) { - lines.push(`${prefix}${key}:`) - for (const item of value) { + lines.push(prefix + key + ':') + for (var ai = 0; ai < value.length; ai++) { + var item = value[ai] if (typeof item === 'object' && item !== null) { - 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}`) - } - } + lines.push(renderArrayObject(item, indent + 1)) } else { - lines.push(`${prefix} - ${item}`) + lines.push(prefix + ' - ' + item) } } } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) + lines.push(prefix + key + ':') lines.push(toYaml(value, indent + 1)) } else { - lines.push(`${prefix}${key}: ${value}`) + lines.push(prefix + key + ': ' + value) + } + } + return lines.join('\n') +} + +function renderArrayObject(item, indent) { + var prefix = '' + for (var p = 0; p < indent; p++) prefix += ' ' + var entries = Object.entries(item).filter(function(pair) { return pair[1] !== null && pair[1] !== undefined && pair[1] !== '' }) + if (entries.length === 0) return '' + var lines = [] + var first = entries[0] + var fk = first[0], fv = first[1] + if (Array.isArray(fv)) { + lines.push(prefix + '- ' + fk + ':') + for (var ai = 0; ai < fv.length; ai++) { + var arrItem = fv[ai] + if (typeof arrItem === 'object' && arrItem !== null) { + lines.push(renderArrayObject(arrItem, indent + 2)) + } else { + lines.push(prefix + ' - ' + arrItem) + } + } + } else if (typeof fv === 'object' && fv !== null) { + lines.push(prefix + '- ' + fk + ':') + lines.push(toYaml(fv, indent + 2)) + } else { + lines.push(prefix + '- ' + fk + ': ' + fv) + } + for (var i = 1; i < entries.length; i++) { + var k = entries[i][0], v = entries[i][1] + if (v === null || v === undefined || v === '') continue + if (Array.isArray(v)) { + lines.push(prefix + ' ' + k + ':') + for (var ai = 0; ai < v.length; ai++) { + var arrItem = v[ai] + if (typeof arrItem === 'object' && arrItem !== null) { + lines.push(renderArrayObject(arrItem, indent + 2)) + } else { + lines.push(prefix + ' - ' + arrItem) + } + } + } else if (typeof v === 'object') { + lines.push(prefix + ' ' + k + ':') + lines.push(toYaml(v, indent + 2)) + } else { + lines.push(prefix + ' ' + k + ': ' + v) } } return lines.join('\n') } export function generateK8sJobYaml(config) { - const container = { + var container = { name: config.name, image: config.image, imagePullPolicy: config.imagePullPolicy, @@ -270,9 +310,9 @@ export function generateK8sJobYaml(config) { // 环境变量 if (config.enableEnv && config.envVars) { - container.env = config.envVars.split('\n').filter(Boolean).map(line => { - const [k, ...v] = line.split('=') - return { name: k.trim(), value: v.join('=').trim() } + container.env = config.envVars.split('\n').filter(Boolean).map(function(line) { + var idx = line.indexOf('=') + return { name: line.substring(0, idx).trim(), value: line.substring(idx + 1).trim() } }) } if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) { @@ -282,11 +322,11 @@ export function generateK8sJobYaml(config) { } // 卷挂载 - const volumes = [] + var volumes = [] if (config.enableVolumeMount) { container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }] - const vol = { name: config.volumeName } - const vt = config.volumeType + var vol = { name: config.volumeName } + var vt = config.volumeType if (vt === 'emptyDir') vol.emptyDir = {} else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName } else if (vt === 'configMap') vol.configMap = { name: config.configMapName } @@ -306,7 +346,7 @@ export function generateK8sJobYaml(config) { } } - const podSpec = { + var podSpec = { containers: [container], restartPolicy: config.restartPolicy, } @@ -322,7 +362,7 @@ export function generateK8sJobYaml(config) { podSpec.securityContext = { fsGroup: config.fsGroup } } - const job = { + var job = { apiVersion: 'batch/v1', kind: 'Job', metadata: { @@ -341,7 +381,7 @@ export function generateK8sJobYaml(config) { }, } - const header = `# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'job.yaml'}\n` + var header = '# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ' + new Date().toLocaleString('zh-CN') + '\n# 部署命令: kubectl apply -f ' + (config.fileName || 'job.yaml') + '\n' return header + '\n' + toYaml(job) }