/** * K8s CronJob 生产级 Schema — 由 ConfTemplate 生成 * 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount */ export const k8sCronJobSchema = { id: 'k8s-cronjob', name: 'K8s CronJob', icon: 'Box', category: 'K8S 工作负载', description: 'Kubernetes CronJob — 定时调度批处理任务(生产级)', format: 'yaml', fileName: 'cronjob.yaml', groups: [ { title: '基础信息', fields: [ { key: 'name', label: 'CronJob 名称', type: 'text', placeholder: 'my-cronjob', default: 'my-cronjob', required: true, }, { key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default', }, { key: 'image', label: '容器镜像', type: 'text', placeholder: 'busybox:latest', default: 'busybox:latest', required: true, }, { key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [ { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' }, { label: 'Always (每次拉取)', value: 'Always' }, { label: 'Never (仅本地)', value: 'Never' }, ], default: 'IfNotPresent', }, { key: 'command', label: 'Command', type: 'text', placeholder: 'echo hello', default: 'echo hello', required: true, tip: '容器启动命令', }, ], }, { title: '调度配置', fields: [ { key: 'schedule', label: 'Cron 表达式', type: 'text', placeholder: '0 * * * *', default: '0 * * * *', required: true, tip: '标准 cron 格式: 分 时 日 月 周', }, { key: 'concurrencyPolicy', label: '并发策略', type: 'select', options: [ { label: 'Allow - 允许并发', value: 'Allow' }, { label: 'Replace - 替换上一个', value: 'Replace' }, { label: 'Forbid - 禁止并发', value: 'Forbid' }, ], default: 'Allow', }, { key: 'suspend', label: '暂停调度', type: 'switch', default: false, tip: '暂停后不再创建新的 Job', }, ], }, { title: '历史与清理', fields: [ { key: 'successfulJobsHistoryLimit', label: '成功 Job 保留数', type: 'number', min: 0, max: 10, default: 3, }, { key: 'failedJobsHistoryLimit', label: '失败 Job 保留数', type: 'number', min: 0, max: 10, default: 1, }, ], }, { title: 'Job 配置', fields: [ { key: 'restartPolicy', label: '重启策略', type: 'select', options: [ { label: 'OnFailure (推荐)', value: 'OnFailure' }, { label: 'Never', value: 'Never' }, ], default: 'OnFailure', }, { key: 'backoffLimit', label: '重试次数上限', type: 'number', min: 0, max: 100, default: 6, }, { key: 'activeDeadlineSeconds', label: '超时时间 (秒)', type: 'number', min: 0, max: 86400, default: 0, tip: '0 表示不限制超时', }, { key: 'ttlSecondsAfterFinished', label: '自动清理 (秒)', type: 'number', min: 0, max: 86400, default: 0, tip: '完成后自动删除 Job,0 表示保留', }, ], }, { title: '容器端口', fields: [ { key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 }, { key: 'portName', label: '端口名称', type: 'text', default: 'http' }, { key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' }, ], }, { title: '资源限制', fields: [ { key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [ { label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' }, { label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' }, ], default: '100m' }, { key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [ { label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' }, { label: '2', value: '2' },{ label: '4', value: '4' }, ], default: '500m' }, { key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [ { label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' }, { label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' }, ], default: '128Mi' }, { key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [ { label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' }, { label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },{ label: '4Gi', value: '4Gi' }, ], default: '256Mi' }, ], }, { title: '环境变量', fields: [ { key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false }, { key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'APP_ENV=production\nLOG_LEVEL=info', default: 'APP_ENV=production\nLOG_LEVEL=info', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' }, { key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false }, { key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } }, { key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false }, { key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } }, ], }, { title: '卷挂载', fields: [ { key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false }, { key: 'volumeType', label: '卷类型', type: 'select', options: [ { label: 'emptyDir - 临时目录', value: 'emptyDir' }, { label: 'PersistentVolumeClaim', value: 'pvc' }, { label: 'ConfigMap', value: 'configMap' }, { label: 'Secret', value: 'secret' }, { label: 'HostPath - 主机路径', value: 'hostPath' }, ], default: 'emptyDir', dependsOn: { key: 'enableVolumeMount', value: true } }, { key: 'volumeName', label: '卷名称', type: 'text', default: 'data', dependsOn: { key: 'enableVolumeMount', value: true } }, { key: 'volumeMountPath', label: '挂载路径', type: 'text', default: '/data', dependsOn: { key: 'enableVolumeMount', value: true } }, { key: 'volumeReadOnly', label: '只读挂载', type: 'switch', default: false, dependsOn: { key: 'enableVolumeMount', value: true } }, { key: 'pvcClaimName', label: 'PVC 名称', type: 'text', default: 'my-pvc', dependsOn: { key: 'volumeType', value: 'pvc' } }, { key: 'configMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'volumeType', value: 'configMap' } }, { key: 'secretVolumeName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'volumeType', value: 'secret' } }, { key: 'hostPath', label: '主机路径', type: 'text', default: '/tmp/data', dependsOn: { key: 'volumeType', value: 'hostPath' } }, ], }, { title: '安全上下文 (Security Context)', fields: [ { key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false }, { key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } }, { key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } }, { key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } }, { key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' }, { key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } }, { key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' }, ], }, { title: 'ServiceAccount', fields: [ { key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' }, ], }, ], } function toYaml(obj, indent = 0) { 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 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}`) } } } else { lines.push(`${prefix} - ${item}`) } } } else if (typeof value === 'object') { lines.push(`${prefix}${key}:`) lines.push(toYaml(value, indent + 1)) } else { lines.push(`${prefix}${key}: ${value}`) } } return lines.join('\n') } export function generateK8sCronJobYaml(config) { const container = { name: config.name, image: config.image, imagePullPolicy: config.imagePullPolicy, command: config.command.split(' '), } // 容器端口 if (config.containerPort) { container.ports = [{ name: config.portName || 'http', containerPort: config.containerPort, protocol: config.protocol || 'TCP', }] } // 资源限制 container.resources = { requests: { cpu: config.cpuRequest, memory: config.memoryRequest }, limits: { cpu: config.cpuLimit, memory: config.memoryLimit }, } // 环境变量 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() } }) } if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) { container.envFrom = [] if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } }) if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } }) } // 卷挂载 const 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 if (vt === 'emptyDir') vol.emptyDir = {} else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName } else if (vt === 'configMap') vol.configMap = { name: config.configMapName } else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName } else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' } volumes.push(vol) } // Security Context if (config.enableSecurityContext) { container.securityContext = { runAsUser: config.runAsUser, runAsGroup: config.runAsGroup, runAsNonRoot: config.runAsNonRoot, readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined, allowPrivilegeEscalation: config.allowPrivilegeEscalation, } } const podSpec = { containers: [container], restartPolicy: config.restartPolicy, } // ServiceAccount if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName // Volumes if (volumes.length) podSpec.volumes = volumes // fsGroup if (config.enableSecurityContext && config.fsGroup) { podSpec.securityContext = { fsGroup: config.fsGroup } } const cronjob = { apiVersion: 'batch/v1', kind: 'CronJob', metadata: { name: config.name, namespace: config.namespace, }, spec: { schedule: config.schedule, concurrencyPolicy: config.concurrencyPolicy, successfulJobsHistoryLimit: config.successfulJobsHistoryLimit, failedJobsHistoryLimit: config.failedJobsHistoryLimit, suspend: config.suspend || undefined, jobTemplate: { spec: { backoffLimit: config.backoffLimit, activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined, ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined, template: { spec: podSpec, }, }, }, }, } const header = `# K8s CronJob - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'cronjob.yaml'}\n` return header + '\n' + toYaml(cronjob) }