6426eedab5
Dockerfile 支持: - 基础镜像 (FROM) + 多阶段构建 + 平台架构 - 元数据 LABEL / 构建参数 ARG - 环境变量 ENV / 工作目录 WORKDIR - 文件复制 COPY + chown/chmod + ADD (自动解压/URL) - 依赖安装: apk/apt/yum/dnf 包管理器 + 应用依赖 + 构建命令 - 端口 EXPOSE / 数据卷 VOLUME - 用户 USER (非 root 运行) - 健康检查 HEALTHCHECK (interval/timeout/retries/start-period) - 入口 ENTRYPOINT / CMD (exec/shell 格式) - 停止信号 STOPSIGNAL / Shell 指令
424 lines
16 KiB
JavaScript
424 lines
16 KiB
JavaScript
/**
|
||
* 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 权限控制' },
|
||
],
|
||
},
|
||
],
|
||
}
|
||
|
||
// ======================== 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 (var ai = 0; ai < value.length; ai++) {
|
||
var item = value[ai]
|
||
if (typeof item === 'object' && item !== null) {
|
||
lines.push(renderArrayObject(item, indent + 1))
|
||
} 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')
|
||
}
|
||
|
||
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) {
|
||
var 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(function(line) {
|
||
var idx = line.indexOf('=')
|
||
return { name: line.substring(0, idx).trim(), value: line.substring(idx + 1).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 } })
|
||
}
|
||
|
||
// 卷挂载
|
||
var volumes = []
|
||
if (config.enableVolumeMount) {
|
||
container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }]
|
||
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 }
|
||
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,
|
||
}
|
||
}
|
||
|
||
var 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 }
|
||
}
|
||
|
||
var 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,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
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)
|
||
}
|