feat: 8 个核心 schema 生产级增强

K8S 工作负载 (5个):
- StatefulSet: +探针(Startup/Liveness/Readiness) +环境变量 +节点亲和性 +Pod反亲和性 +容忍度 +安全上下文 +ServiceAccount
- DaemonSet: +容器端口 +探针 +环境变量 +卷挂载 +节点亲和性 +安全上下文 +ServiceAccount
- Pod: +探针 +卷挂载(5种类型) +NodeSelector +节点亲和性 +Pod反亲和性 +容忍度 +安全上下文 +HostNetwork +DNSPolicy
- CronJob: +容器端口 +资源限制 +环境变量 +卷挂载 +安全上下文 +ServiceAccount
- Job: +容器端口 +资源限制 +环境变量 +卷挂载 +安全上下文 +ServiceAccount

中间件 (3个):
- Redis: +ACL配置 +慢日志 +延迟监控 +客户端管理(ioThreads/lazyfree)
- PostgreSQL: +连接池 +流复制(GTID/WAL) +Autovacuum调优
- NGINX: +限流(limit_req) +Upstream负载均衡(least_conn/ip_hash) +缓存(proxy_cache)
This commit is contained in:
cnbugs
2026-07-22 15:00:34 +08:00
parent e258969c37
commit 4e98eb8c4a
8 changed files with 1937 additions and 655 deletions
+197 -10
View File
@@ -1,9 +1,14 @@
/**
* K8s CronJob 生产级 Schema — 由 ConfTemplate 生成
* 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount
*/
export const k8sCronJobSchema = { export const k8sCronJobSchema = {
id: 'k8s-cronjob', id: 'k8s-cronjob',
name: 'K8s CronJob', name: 'K8s CronJob',
icon: 'Box', icon: 'Box',
category: 'K8S 工作负载', category: 'K8S 工作负载',
description: 'Kubernetes CronJob — 定时调度批处理任务', description: 'Kubernetes CronJob — 定时调度批处理任务(生产级)',
format: 'yaml', format: 'yaml',
fileName: 'cronjob.yaml', fileName: 'cronjob.yaml',
groups: [ groups: [
@@ -33,6 +38,17 @@ export const k8sCronJobSchema = {
default: 'busybox:latest', default: 'busybox:latest',
required: true, required: true,
}, },
{
key: 'imagePullPolicy',
label: '镜像拉取策略',
type: 'select',
options: [
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
{ label: 'Always (每次拉取)', value: 'Always' },
{ label: 'Never (仅本地)', value: 'Never' },
],
default: 'IfNotPresent',
},
{ {
key: 'command', key: 'command',
label: 'Command', label: 'Command',
@@ -118,6 +134,102 @@ export const k8sCronJobSchema = {
max: 100, max: 100,
default: 6, 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: '完成后自动删除 Job0 表示保留',
},
],
},
{
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 权限控制' },
], ],
}, },
], ],
@@ -126,10 +238,8 @@ export const k8sCronJobSchema = {
function toYaml(obj, indent = 0) { function toYaml(obj, indent = 0) {
const lines = [] const lines = []
const prefix = ' '.repeat(indent) const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined || value === '') continue if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) { if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`) lines.push(`${prefix}${key}:`)
for (const item of value) { for (const item of value) {
@@ -137,9 +247,18 @@ function toYaml(obj, indent = 0) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue if (entries.length === 0) continue
const first = entries[0] const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`) 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++) { for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) 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 { } else {
lines.push(`${prefix} - ${item}`) lines.push(`${prefix} - ${item}`)
@@ -152,7 +271,6 @@ function toYaml(obj, indent = 0) {
lines.push(`${prefix}${key}: ${value}`) lines.push(`${prefix}${key}: ${value}`)
} }
} }
return lines.join('\n') return lines.join('\n')
} }
@@ -160,9 +278,79 @@ export function generateK8sCronJobYaml(config) {
const container = { const container = {
name: config.name, name: config.name,
image: config.image, image: config.image,
imagePullPolicy: config.imagePullPolicy,
command: config.command.split(' '), 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 = { const cronjob = {
apiVersion: 'batch/v1', apiVersion: 'batch/v1',
kind: 'CronJob', kind: 'CronJob',
@@ -179,11 +367,10 @@ export function generateK8sCronJobYaml(config) {
jobTemplate: { jobTemplate: {
spec: { spec: {
backoffLimit: config.backoffLimit, backoffLimit: config.backoffLimit,
activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined,
ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined,
template: { template: {
spec: { spec: podSpec,
containers: [container],
restartPolicy: config.restartPolicy,
},
}, },
}, },
}, },
+338 -188
View File
@@ -10,161 +10,358 @@ export const k8sDaemonSetSchema = {
{ {
title: '基础信息', title: '基础信息',
fields: [ fields: [
{ { key: 'name', label: 'DaemonSet 名称', type: 'text', placeholder: 'my-daemonset', default: 'my-daemonset', required: true },
key: 'name', { key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' },
label: 'DaemonSet 名称', { key: 'appName', label: '应用名称', type: 'text', placeholder: 'my-app', default: 'my-app', required: true },
type: 'text', { key: 'image', label: '容器镜像', type: 'text', placeholder: 'fluentd:latest', default: 'fluentd:latest', required: true },
placeholder: 'my-daemonset', { key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
default: 'my-daemonset', { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
required: true, { label: 'Always (每次拉取)', value: 'Always' },
}, { label: 'Never (仅本地)', value: 'Never' },
{ ], default: 'IfNotPresent' },
key: 'namespace', ],
label: '命名空间', },
type: 'text', {
placeholder: 'default', title: '容器端口',
default: 'default', fields: [
}, { key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 8080 },
{ { key: 'portName', label: '端口名称', type: 'text', default: 'http' },
key: 'appName', { key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
label: '应用名称',
type: 'text',
placeholder: 'my-app',
default: 'my-app',
required: true,
},
{
key: 'image',
label: '容器镜像',
type: 'text',
placeholder: 'fluentd:latest',
default: 'fluentd:latest',
required: true,
},
{
key: 'containerPort',
label: '容器端口',
type: 'number',
min: 1,
max: 65535,
default: 8080,
},
], ],
}, },
{ {
title: '资源限制', title: '资源限制',
fields: [ fields: [
{ { key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
key: 'cpuRequest', { label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
label: 'CPU Requests', { label: '500m', value: '500m' },{ label: '1', value: '1' },
type: 'select', ], default: '100m' },
options: [ { key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
{ label: '50m', value: '50m' }, { label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
{ label: '100m', value: '100m' }, { label: '2', value: '2' },
{ label: '200m', value: '200m' }, ], default: '500m' },
{ label: '500m', value: '500m' }, { key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
{ label: '1', value: '1' }, { label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
], { label: '512Mi', value: '512Mi' },
default: '100m', ], default: '128Mi' },
}, { key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
{ { label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
key: 'cpuLimit', { label: '1Gi', value: '1Gi' },
label: 'CPU Limits', ], default: '256Mi' },
type: 'select',
options: [
{ label: '200m', value: '200m' },
{ label: '500m', value: '500m' },
{ label: '1', value: '1' },
{ label: '2', value: '2' },
],
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' },
],
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' },
],
default: '256Mi',
},
], ],
}, },
{ {
title: '节点调度', title: '节点调度',
fields: [ fields: [
{ { key: 'hostNetwork', label: '使用主机网络', type: 'switch', default: false, tip: 'Pod 将使用宿主机网络命名空间' },
key: 'hostNetwork', { key: 'tolerationsEnabled', label: '添加 Toleration', type: 'switch', default: false, tip: '允许在有污点的节点上运行' },
label: '使用主机网络', { key: 'tolerationKey', label: 'Toleration Key', type: 'text', placeholder: 'node-role.kubernetes.io/master', default: '', dependsOn: { key: 'tolerationsEnabled', value: true } },
type: 'switch', { key: 'tolerationValue', label: 'Toleration Value', type: 'text', placeholder: '', default: '', dependsOn: { key: 'tolerationsEnabled', value: true } },
default: false,
tip: 'Pod 将使用宿主机网络命名空间',
},
{
key: 'tolerationsEnabled',
label: '添加 Toleration',
type: 'switch',
default: false,
tip: '允许在有污点的节点上运行',
},
{
key: 'tolerationKey',
label: 'Toleration Key',
type: 'text',
placeholder: 'node-role.kubernetes.io/master',
default: '',
dependsOn: { key: 'tolerationsEnabled', value: true },
},
{
key: 'tolerationValue',
label: 'Toleration Value',
type: 'text',
placeholder: '',
default: '',
dependsOn: { key: 'tolerationsEnabled', value: true },
},
], ],
}, },
{ {
title: '更新策略', title: '更新策略',
fields: [ fields: [
{ { key: 'updateStrategy', label: '更新策略', type: 'select', options: [
key: 'updateStrategy', { label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
label: '更新策略', { label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
type: 'select', ], default: 'RollingUpdate' },
options: [ ],
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' }, },
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' }, {
], title: '启动探针 (Startup Probe)',
default: 'RollingUpdate', fields: [
}, { key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
{ key: 'startupProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
{ key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
{ key: 'startupPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 8080, dependsOn: { key: 'enableStartupProbe', value: true } },
{ key: 'startupCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'startupProbeType', value: 'exec' } },
{ key: 'startupInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 0, dependsOn: { key: 'enableStartupProbe', value: true } },
{ key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
{ key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
],
},
{
title: '存活探针 (Liveness Probe)',
fields: [
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
{ key: 'livenessProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 8080, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 20, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
],
},
{
title: '就绪探针 (Readiness Probe)',
fields: [
{ key: 'enableReadinessProbe', label: '启用就绪探针', type: 'switch', default: true, tip: '失败则从 Service 端点摘除' },
{ key: 'readinessProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 8080, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
{ key: 'readinessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
{ key: 'readinessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 5, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', value: true } },
],
},
{
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: 'hostPath - 主机路径', value: 'hostPath' },
{ label: 'ConfigMap', value: 'configMap' },
{ label: 'Secret', value: 'secret' },
], 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: '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: '节点调度 — 节点亲和性 (Node Affinity)',
fields: [
{ key: 'enableNodeAffinity', label: '启用节点亲和性', type: 'switch', default: false, tip: '比 NodeSelector 更灵活的调度规则' },
{ key: 'nodeAffinityType', label: '亲和类型', type: 'select', options: [
{ label: 'required - 必须满足 (硬性)', value: 'required' },
{ label: 'preferred - 尽量满足 (软性)', value: 'preferred' },
], default: 'required', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityKey1', label: '匹配标签 Key', type: 'text', placeholder: 'topology.kubernetes.io/zone', default: 'topology.kubernetes.io/zone', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityOperator1', label: '操作符', type: 'select', options: [
{ label: 'In - 值在列表中', value: 'In' },
{ label: 'NotIn - 值不在列表中', value: 'NotIn' },
{ label: 'Exists - 标签存在', value: 'Exists' },
{ label: 'DoesNotExist - 标签不存在', value: 'DoesNotExist' },
{ label: 'Gt - 大于', value: 'Gt' },
{ label: 'Lt - 小于', value: 'Lt' },
], default: 'In', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityValues1', label: '匹配值 (逗号分隔)', type: 'text', placeholder: 'cn-east-1a,cn-east-1b', default: 'cn-east-1a,cn-east-1b', dependsOn: { key: 'nodeAffinityOperator1', valueNotIn: ['Exists', 'DoesNotExist'] } },
{ key: 'nodeAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 80, dependsOn: { key: 'nodeAffinityType', value: 'preferred' } },
],
},
{
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: '标签与注解',
fields: [
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用应用名称' },
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
], ],
}, },
], ],
} }
// ======================== YAML 生成器 ========================
function buildProbe(config, prefix, type, path, port, cmd, headers, initDelay, period, timeout, failure, success) {
if (!config[prefix]) return null
const probeType = config[type] || 'httpGet'
const probe = {}
if (probeType === 'httpGet') {
probe.httpGet = { path: config[path], port: config[port], scheme: 'HTTP' }
const h = config[headers]
if (h) {
const hdrs = h.split('\n').filter(Boolean).map(line => {
const [k, ...v] = line.split(':')
return { name: k.trim(), value: v.join(':').trim() }
})
if (hdrs.length) probe.httpGet.httpHeaders = hdrs
}
} else if (probeType === 'tcpSocket') {
probe.tcpSocket = { port: config[port] }
} else {
const c = config[cmd] || 'cat /tmp/healthy'
probe.exec = { command: c.split(' ') }
}
probe.initialDelaySeconds = config[initDelay]
probe.periodSeconds = config[period]
probe.timeoutSeconds = config[timeout]
probe.failureThreshold = config[failure]
if (success !== null) probe.successThreshold = config[success]
return probe
}
function buildNodeAffinity(config) {
if (!config.enableNodeAffinity) return undefined
const key = config.nodeAffinityKey1
const op = config.nodeAffinityOperator1
const vals = (config.nodeAffinityValues1 || '').split(',').map(s => s.trim()).filter(Boolean)
const term = { matchExpressions: [{ key, operator: op }] }
if (['In', 'NotIn'].includes(op)) term.matchExpressions[0].values = vals
const affinity = {}
if (config.nodeAffinityType === 'required') {
affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [term] } }
} else {
affinity.nodeAffinity = {
preferredDuringSchedulingIgnoredDuringExecution: [{
weight: config.nodeAffinityWeight || 80,
preference: term,
}],
}
}
return affinity
}
export function generateK8sDaemonSetYaml(config) {
const labels = { app: config.appLabel || config.appName }
if (config.versionLabel) labels.version = config.versionLabel
const container = {
name: config.appName,
image: config.image,
imagePullPolicy: config.imagePullPolicy,
ports: [{ name: config.portName, containerPort: config.containerPort, protocol: config.protocol }],
resources: {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
},
}
// 探针
container.startupProbe = buildProbe(config, 'enableStartupProbe', 'startupProbeType', 'startupPath', 'startupPort', 'startupCommand', null, 'startupInitialDelay', 'startupPeriod', 'startupTimeout', 'startupFailureThreshold', null)
container.livenessProbe = buildProbe(config, 'enableLivenessProbe', 'livenessProbeType', 'livenessPath', 'livenessPort', 'livenessCommand', 'livenessHttpHeaders', 'livenessInitialDelay', 'livenessPeriod', 'livenessTimeout', 'livenessFailureThreshold', 'livenessSuccessThreshold')
container.readinessProbe = buildProbe(config, 'enableReadinessProbe', 'readinessProbeType', 'readinessPath', 'readinessPort', 'readinessCommand', 'readinessHttpHeaders', 'readinessInitialDelay', 'readinessPeriod', 'readinessTimeout', 'readinessFailureThreshold', 'readinessSuccessThreshold')
if (!container.startupProbe) delete container.startupProbe
if (!container.livenessProbe) delete container.livenessProbe
if (!container.readinessProbe) delete container.readinessProbe
// 环境变量
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 } })
}
// Security Context (container-level)
if (config.enableSecurityContext) {
container.securityContext = {
runAsUser: config.runAsUser,
runAsGroup: config.runAsGroup,
runAsNonRoot: config.runAsNonRoot,
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
}
}
// 卷挂载
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 === '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)
}
const podSpec = { containers: [container] }
if (config.hostNetwork) podSpec.hostNetwork = true
// ServiceAccount
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
// 优雅终止
if (config.terminationGracePeriod) podSpec.terminationGracePeriodSeconds = config.terminationGracePeriod
// Tolerations (existing simple + new advanced)
if (config.tolerationsEnabled && config.tolerationKey) {
const toleration = { key: config.tolerationKey, operator: 'Exists', effect: 'NoSchedule' }
if (config.tolerationValue) {
toleration.operator = 'Equal'
toleration.value = config.tolerationValue
}
podSpec.tolerations = [toleration]
}
// Node Affinity
const affinity = buildNodeAffinity(config)
if (affinity) podSpec.affinity = affinity
// Volumes
if (volumes.length) podSpec.volumes = volumes
// fsGroup
if (config.enableSecurityContext && config.fsGroup) {
podSpec.securityContext = { fsGroup: config.fsGroup }
}
const daemonset = {
apiVersion: 'apps/v1',
kind: 'DaemonSet',
metadata: { name: config.name, namespace: config.namespace, labels },
spec: {
selector: { matchLabels: { app: config.appLabel || config.appName } },
updateStrategy: config.updateStrategy === 'RollingUpdate'
? { type: 'RollingUpdate', rollingUpdate: { maxUnavailable: 1 } }
: { type: 'OnDelete' },
template: { metadata: { labels }, spec: podSpec },
},
}
const header = `# K8s DaemonSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'daemonset.yaml'}\n`
return header + '\n' + toYaml(daemonset)
}
function toYaml(obj, indent = 0) { function toYaml(obj, indent = 0) {
const lines = [] const lines = []
const prefix = ' '.repeat(indent) const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined || value === '') continue if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) { if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`) lines.push(`${prefix}${key}:`)
for (const item of value) { for (const item of value) {
@@ -172,9 +369,18 @@ function toYaml(obj, indent = 0) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue if (entries.length === 0) continue
const first = entries[0] const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`) 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++) { for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) 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 { } else {
lines.push(`${prefix} - ${item}`) lines.push(`${prefix} - ${item}`)
@@ -187,61 +393,5 @@ function toYaml(obj, indent = 0) {
lines.push(`${prefix}${key}: ${value}`) lines.push(`${prefix}${key}: ${value}`)
} }
} }
return lines.join('\n') return lines.join('\n')
} }
export function generateK8sDaemonSetYaml(config) {
const labels = { app: config.appName }
const container = {
name: config.appName,
image: config.image,
ports: [{ containerPort: config.containerPort, name: 'http' }],
resources: {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
},
}
const podSpec = {
containers: [container],
}
if (config.hostNetwork) {
podSpec.hostNetwork = true
}
if (config.tolerationsEnabled && config.tolerationKey) {
const toleration = { key: config.tolerationKey, operator: 'Exists', effect: 'NoSchedule' }
if (config.tolerationValue) {
toleration.operator = 'Equal'
toleration.value = config.tolerationValue
}
podSpec.tolerations = [toleration]
}
const daemonset = {
apiVersion: 'apps/v1',
kind: 'DaemonSet',
metadata: {
name: config.name,
namespace: config.namespace,
labels,
},
spec: {
selector: { matchLabels: { app: config.appName } },
updateStrategy: config.updateStrategy === 'RollingUpdate'
? { type: 'RollingUpdate', rollingUpdate: { maxUnavailable: 1 } }
: { type: 'OnDelete' },
template: {
metadata: { labels },
spec: podSpec,
},
},
}
const header = `# K8s DaemonSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'daemonset.yaml'}\n`
return header + '\n' + toYaml(daemonset)
}
+179 -18
View File
@@ -1,9 +1,14 @@
/**
* K8s Job 生产级 Schema — 由 ConfTemplate 生成
* 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount
*/
export const k8sJobSchema = { export const k8sJobSchema = {
id: 'k8s-job', id: 'k8s-job',
name: 'K8s Job', name: 'K8s Job',
icon: 'Box', icon: 'Box',
category: 'K8S 工作负载', category: 'K8S 工作负载',
description: 'Kubernetes Job — 运行一次性批处理任务', description: 'Kubernetes Job — 运行一次性批处理任务(生产级)',
format: 'yaml', format: 'yaml',
fileName: 'job.yaml', fileName: 'job.yaml',
groups: [ groups: [
@@ -33,6 +38,17 @@ export const k8sJobSchema = {
default: 'busybox:latest', default: 'busybox:latest',
required: true, required: true,
}, },
{
key: 'imagePullPolicy',
label: '镜像拉取策略',
type: 'select',
options: [
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
{ label: 'Always (每次拉取)', value: 'Always' },
{ label: 'Never (仅本地)', value: 'Never' },
],
default: 'IfNotPresent',
},
{ {
key: 'command', key: 'command',
label: 'Command', label: 'Command',
@@ -109,16 +125,92 @@ export const k8sJobSchema = {
}, },
], ],
}, },
{
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) { function toYaml(obj, indent = 0) {
const lines = [] const lines = []
const prefix = ' '.repeat(indent) const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined || value === '') continue if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) { if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`) lines.push(`${prefix}${key}:`)
for (const item of value) { for (const item of value) {
@@ -126,9 +218,18 @@ function toYaml(obj, indent = 0) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue if (entries.length === 0) continue
const first = entries[0] const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`) 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++) { for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) 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 { } else {
lines.push(`${prefix} - ${item}`) lines.push(`${prefix} - ${item}`)
@@ -141,7 +242,6 @@ function toYaml(obj, indent = 0) {
lines.push(`${prefix}${key}: ${value}`) lines.push(`${prefix}${key}: ${value}`)
} }
} }
return lines.join('\n') return lines.join('\n')
} }
@@ -149,9 +249,79 @@ export function generateK8sJobYaml(config) {
const container = { const container = {
name: config.name, name: config.name,
image: config.image, image: config.image,
imagePullPolicy: config.imagePullPolicy,
command: config.command.split(' '), 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 job = { const job = {
apiVersion: 'batch/v1', apiVersion: 'batch/v1',
kind: 'Job', kind: 'Job',
@@ -163,23 +333,14 @@ export function generateK8sJobYaml(config) {
backoffLimit: config.backoffLimit, backoffLimit: config.backoffLimit,
completions: config.completions, completions: config.completions,
parallelism: config.parallelism, parallelism: config.parallelism,
activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined,
ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined,
template: { template: {
spec: { spec: podSpec,
containers: [container],
restartPolicy: config.restartPolicy,
},
}, },
}, },
} }
if (config.activeDeadlineSeconds > 0) {
job.spec.activeDeadlineSeconds = config.activeDeadlineSeconds
}
if (config.ttlSecondsAfterFinished > 0) {
job.spec.ttlSecondsAfterFinished = config.ttlSecondsAfterFinished
}
const header = `# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'job.yaml'}\n` const header = `# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'job.yaml'}\n`
return header + '\n' + toYaml(job) return header + '\n' + toYaml(job)
+411 -230
View File
@@ -10,217 +10,446 @@ export const k8sPodSchema = {
{ {
title: '基础信息', title: '基础信息',
fields: [ fields: [
{ { key: 'name', label: 'Pod 名称', type: 'text', placeholder: 'my-pod', default: 'my-pod', required: true },
key: 'name', { key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' },
label: 'Pod 名称', { key: 'containerName', label: '容器名称', type: 'text', placeholder: 'main', default: 'main', required: true },
type: 'text', { key: 'image', label: '容器镜像', type: 'text', placeholder: 'nginx:1.24', default: 'nginx:1.24', required: true },
placeholder: 'my-pod', { key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
default: 'my-pod', { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
required: true, { label: 'Always', value: 'Always' },
}, { label: 'Never', value: 'Never' },
{ ], default: 'IfNotPresent' },
key: 'namespace',
label: '命名空间',
type: 'text',
placeholder: 'default',
default: 'default',
},
{
key: 'containerName',
label: '容器名称',
type: 'text',
placeholder: 'main',
default: 'main',
required: true,
},
{
key: 'image',
label: '容器镜像',
type: 'text',
placeholder: 'nginx:1.24',
default: 'nginx:1.24',
required: true,
},
{
key: 'imagePullPolicy',
label: '镜像拉取策略',
type: 'select',
options: [
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
{ label: 'Always', value: 'Always' },
{ label: 'Never', value: 'Never' },
],
default: 'IfNotPresent',
},
], ],
}, },
{ {
title: '容器端口', title: '容器端口',
fields: [ fields: [
{ { key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
key: 'containerPort', { key: 'portName', label: '端口名称', type: 'text', default: 'http' },
label: '容器端口', { key: 'protocol', label: '协议', type: 'select', options: [
type: 'number', { label: 'TCP', value: 'TCP' },
min: 1, { label: 'UDP', value: 'UDP' },
max: 65535, ], default: 'TCP' },
default: 80,
},
{
key: 'protocol',
label: '协议',
type: 'select',
options: [
{ label: 'TCP', value: 'TCP' },
{ label: 'UDP', value: 'UDP' },
],
default: 'TCP',
},
], ],
}, },
{ {
title: '启动命令 (可选)', title: '启动命令 (可选)',
fields: [ fields: [
{ { key: 'command', label: 'Command', type: 'text', placeholder: '/bin/sh', default: '', tip: '容器 ENTRYPOINT,留空则使用镜像默认值' },
key: 'command', { key: 'args', label: 'Args', type: 'text', placeholder: '-c, echo hello', default: '', tip: '逗号分隔多个参数' },
label: 'Command',
type: 'text',
placeholder: '/bin/sh',
default: '',
tip: '容器 ENTRYPOINT,留空则使用镜像默认值',
},
{
key: 'args',
label: 'Args',
type: 'text',
placeholder: '-c, echo hello',
default: '',
tip: '逗号分隔多个参数',
},
], ],
}, },
{ {
title: '环境变量 (可选)', title: '环境变量 (可选)',
fields: [ fields: [
{ { key: 'envVars', label: '环境变量', type: 'keyvalue', default: [], tip: '键值对,将注入为容器环境变量' },
key: 'envVars', { key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false },
label: '环境变量', { key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } },
type: 'keyvalue', { key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
default: [], { key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
tip: '键值对,将注入为容器环境变量',
},
], ],
}, },
{ {
title: '资源限制', title: '资源限制',
fields: [ fields: [
{ { key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
key: 'cpuRequest', { label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
label: 'CPU Requests', { label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
type: 'select', ], default: '100m' },
options: [ { key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
{ label: '50m', value: '50m' }, { label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
{ label: '100m', value: '100m' }, { label: '2', value: '2' },{ label: '4', value: '4' },
{ label: '200m', value: '200m' }, ], default: '500m' },
{ label: '500m', value: '500m' }, { key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
{ label: '1', value: '1' }, { label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
{ label: '2', value: '2' }, { label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
], ], default: '128Mi' },
default: '100m', { 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' },
key: 'cpuLimit', ], default: '256Mi' },
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' },
],
default: '256Mi',
},
], ],
}, },
{ {
title: '重启与调度', title: '重启与调度',
fields: [ fields: [
{ { key: 'restartPolicy', label: '重启策略', type: 'select', options: [
key: 'restartPolicy', { label: 'Always (默认)', value: 'Always' },
label: '重启策略', { label: 'OnFailure', value: 'OnFailure' },
type: 'select', { label: 'Never', value: 'Never' },
options: [ ], default: 'Always' },
{ label: 'Always (默认)', value: 'Always' }, { key: 'nodeName', label: '指定节点', type: 'text', placeholder: 'node-1', default: '', tip: '留空则由调度器自动分配' },
{ label: 'OnFailure', value: 'OnFailure' },
{ label: 'Never', value: 'Never' },
],
default: 'Always',
},
{
key: 'nodeName',
label: '指定节点',
type: 'text',
placeholder: 'node-1',
default: '',
tip: '留空则由调度器自动分配',
},
], ],
}, },
{ {
title: '标签', title: '标签',
fields: [ fields: [
{ { key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用 Pod 名称' },
key: 'appLabel', { key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
label: 'app 标签值', ],
type: 'text', },
default: '', {
tip: '留空则使用 Pod 名称', title: '启动探针 (Startup Probe)',
}, fields: [
{ { key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
key: 'versionLabel', { key: 'startupProbeType', label: '探针方式', type: 'select', options: [
label: 'version 标签', { label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
type: 'text', ], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
placeholder: 'v1', { key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
default: 'v1', { key: 'startupPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableStartupProbe', value: true } },
}, { key: 'startupCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'startupProbeType', value: 'exec' } },
{ key: 'startupInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 0, dependsOn: { key: 'enableStartupProbe', value: true } },
{ key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
{ key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
],
},
{
title: '存活探针 (Liveness Probe)',
fields: [
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
{ key: 'livenessProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 20, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
],
},
{
title: '就绪探针 (Readiness Probe)',
fields: [
{ key: 'enableReadinessProbe', label: '启用就绪探针', type: 'switch', default: true, tip: '失败则从 Service 端点摘除' },
{ key: 'readinessProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
{ key: 'readinessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
{ key: 'readinessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 5, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', 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: '节点调度 — NodeSelector',
fields: [
{ key: 'enableNodeSelector', label: '启用 NodeSelector', type: 'switch', default: false, tip: '简单的节点标签匹配' },
{ key: 'nodeSelectorKey', label: '标签 Key', type: 'text', placeholder: 'kubernetes.io/os', default: 'kubernetes.io/os', dependsOn: { key: 'enableNodeSelector', value: true } },
{ key: 'nodeSelectorValue', label: '标签 Value', type: 'text', placeholder: 'linux', default: 'linux', dependsOn: { key: 'enableNodeSelector', value: true } },
],
},
{
title: '节点调度 — 节点亲和性 (Node Affinity)',
fields: [
{ key: 'enableNodeAffinity', label: '启用节点亲和性', type: 'switch', default: false, tip: '比 NodeSelector 更灵活的调度规则' },
{ key: 'nodeAffinityType', label: '亲和类型', type: 'select', options: [
{ label: 'required - 必须满足 (硬性)', value: 'required' },
{ label: 'preferred - 尽量满足 (软性)', value: 'preferred' },
], default: 'required', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityKey1', label: '匹配标签 Key', type: 'text', placeholder: 'topology.kubernetes.io/zone', default: 'topology.kubernetes.io/zone', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityOperator1', label: '操作符', type: 'select', options: [
{ label: 'In - 值在列表中', value: 'In' },
{ label: 'NotIn - 值不在列表中', value: 'NotIn' },
{ label: 'Exists - 标签存在', value: 'Exists' },
{ label: 'DoesNotExist - 标签不存在', value: 'DoesNotExist' },
{ label: 'Gt - 大于', value: 'Gt' },
{ label: 'Lt - 小于', value: 'Lt' },
], default: 'In', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityValues1', label: '匹配值 (逗号分隔)', type: 'text', placeholder: 'cn-east-1a,cn-east-1b', default: 'cn-east-1a,cn-east-1b', dependsOn: { key: 'nodeAffinityOperator1', valueNotIn: ['Exists', 'DoesNotExist'] } },
{ key: 'nodeAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 80, dependsOn: { key: 'nodeAffinityType', value: 'preferred' } },
],
},
{
title: 'Pod 反亲和性 (Pod Anti-Affinity)',
fields: [
{ key: 'enablePodAntiAffinity', label: '启用 Pod 反亲和性', type: 'switch', default: false, tip: '让副本分散到不同节点/区域,提高可用性' },
{ key: 'podAntiAffinityType', label: '亲和类型', type: 'select', options: [
{ label: 'required - 必须分散 (硬性)', value: 'required' },
{ label: 'preferred - 尽量分散 (软性)', value: 'preferred' },
], default: 'preferred', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
{ key: 'podAntiAffinityKey', label: '匹配标签 Key', type: 'text', default: 'app', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
{ key: 'podAntiAffinityTopology', label: '拓扑域', type: 'select', options: [
{ label: 'kubernetes.io/hostname (节点级)', value: 'kubernetes.io/hostname' },
{ label: 'topology.kubernetes.io/zone (可用区级)', value: 'topology.kubernetes.io/zone' },
{ label: 'topology.kubernetes.io/region (地域级)', value: 'topology.kubernetes.io/region' },
], default: 'kubernetes.io/hostname', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
{ key: 'podAntiAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 100, dependsOn: { key: 'podAntiAffinityType', value: 'preferred' } },
],
},
{
title: '容忍度 (Tolerations)',
fields: [
{ key: 'enableToleration', label: '启用容忍度', type: 'switch', default: false, tip: '允许调度到有 taint 的节点' },
{ key: 'tolerationKey', label: 'Taint Key', type: 'text', placeholder: 'node-role.kubernetes.io/master', default: 'node-role.kubernetes.io/master', dependsOn: { key: 'enableToleration', value: true } },
{ key: 'tolerationOperator', label: '操作符', type: 'select', options: [
{ label: 'Equal - 值匹配', value: 'Equal' },
{ label: 'Exists - Key 存在即可', value: 'Exists' },
], default: 'Exists', dependsOn: { key: 'enableToleration', value: true } },
{ key: 'tolerationValue', label: 'Taint Value', type: 'text', default: '', dependsOn: { key: 'tolerationOperator', value: 'Equal' } },
{ key: 'tolerationEffect', label: 'Taint Effect', type: 'select', options: [
{ label: 'NoSchedule - 不调度新 Pod', value: 'NoSchedule' },
{ label: 'PreferNoSchedule - 尽量不调度', value: 'PreferNoSchedule' },
{ label: 'NoExecute - 驱逐已有 Pod', value: 'NoExecute' },
], default: 'NoSchedule', dependsOn: { key: 'enableToleration', value: true } },
{ key: 'tolerationSeconds', label: '容忍时长 (秒)', type: 'number', min: 0, max: 86400, default: 300, dependsOn: { key: 'tolerationEffect', value: 'NoExecute' }, tip: '仅 NoExecute 有效' },
],
},
{
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: '高级选项',
fields: [
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
{ key: 'hostNetwork', label: '使用主机网络', type: 'switch', default: false, tip: 'Pod 将使用宿主机网络命名空间' },
{ key: 'dnsPolicy', label: 'DNS 策略', type: 'select', options: [
{ label: 'ClusterFirst (默认)', value: 'ClusterFirst' },
{ label: 'Default - 继承节点 DNS', value: 'Default' },
{ label: 'ClusterFirstWithHostNet - hostNetwork 时集群优先', value: 'ClusterFirstWithHostNet' },
{ label: 'None - 使用 dnsConfig', value: 'None' },
], default: 'ClusterFirst' },
], ],
}, },
], ],
} }
// ======================== YAML 生成器 ========================
function buildProbe(config, prefix, type, path, port, cmd, headers, initDelay, period, timeout, failure, success) {
if (!config[prefix]) return null
const probeType = config[type] || 'httpGet'
const probe = {}
if (probeType === 'httpGet') {
probe.httpGet = { path: config[path], port: config[port], scheme: 'HTTP' }
const h = config[headers]
if (h) {
const hdrs = h.split('\n').filter(Boolean).map(line => {
const [k, ...v] = line.split(':')
return { name: k.trim(), value: v.join(':').trim() }
})
if (hdrs.length) probe.httpGet.httpHeaders = hdrs
}
} else if (probeType === 'tcpSocket') {
probe.tcpSocket = { port: config[port] }
} else {
const c = config[cmd] || 'cat /tmp/healthy'
probe.exec = { command: c.split(' ') }
}
probe.initialDelaySeconds = config[initDelay]
probe.periodSeconds = config[period]
probe.timeoutSeconds = config[timeout]
probe.failureThreshold = config[failure]
if (success !== null) probe.successThreshold = config[success]
return probe
}
function buildAffinity(config) {
const affinity = {}
// Node Affinity
if (config.enableNodeAffinity) {
const key = config.nodeAffinityKey1
const op = config.nodeAffinityOperator1
const vals = (config.nodeAffinityValues1 || '').split(',').map(s => s.trim()).filter(Boolean)
const term = { matchExpressions: [{ key, operator: op }] }
if (['In', 'NotIn'].includes(op)) term.matchExpressions[0].values = vals
if (config.nodeAffinityType === 'required') {
affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [term] } }
} else {
affinity.nodeAffinity = {
preferredDuringSchedulingIgnoredDuringExecution: [{
weight: config.nodeAffinityWeight || 80,
preference: term,
}],
}
}
}
// Pod Anti-Affinity
if (config.enablePodAntiAffinity) {
const labelKey = config.podAntiAffinityKey || 'app'
const topology = config.podAntiAffinityTopology || 'kubernetes.io/hostname'
const term = {
labelSelector: { matchExpressions: [{ key: labelKey, operator: 'In', values: [config.appLabel || config.name] }] },
topologyKey: topology,
}
if (config.podAntiAffinityType === 'required') {
affinity.podAntiAffinity = { requiredDuringSchedulingIgnoredDuringExecution: [term] }
} else {
affinity.podAntiAffinity = {
preferredDuringSchedulingIgnoredDuringExecution: [{
weight: config.podAntiAffinityWeight || 100,
podAffinityTerm: term,
}],
}
}
}
return Object.keys(affinity).length ? affinity : undefined
}
export function generateK8sPodYaml(config) {
const labels = { app: config.appLabel || config.name }
if (config.versionLabel) labels.version = config.versionLabel
const container = {
name: config.containerName,
image: config.image,
imagePullPolicy: config.imagePullPolicy,
ports: [{ name: config.portName, containerPort: config.containerPort, protocol: config.protocol }],
resources: {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
},
}
if (config.command) container.command = config.command.split(',').map(s => s.trim())
if (config.args) container.args = config.args.split(',').map(s => s.trim())
if (config.envVars && config.envVars.length > 0) {
container.env = config.envVars.map(e => ({ name: e.key, value: e.value }))
}
// envFrom
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 } })
}
// 探针
container.startupProbe = buildProbe(config, 'enableStartupProbe', 'startupProbeType', 'startupPath', 'startupPort', 'startupCommand', null, 'startupInitialDelay', 'startupPeriod', 'startupTimeout', 'startupFailureThreshold', null)
container.livenessProbe = buildProbe(config, 'enableLivenessProbe', 'livenessProbeType', 'livenessPath', 'livenessPort', 'livenessCommand', 'livenessHttpHeaders', 'livenessInitialDelay', 'livenessPeriod', 'livenessTimeout', 'livenessFailureThreshold', 'livenessSuccessThreshold')
container.readinessProbe = buildProbe(config, 'enableReadinessProbe', 'readinessProbeType', 'readinessPath', 'readinessPort', 'readinessCommand', 'readinessHttpHeaders', 'readinessInitialDelay', 'readinessPeriod', 'readinessTimeout', 'readinessFailureThreshold', 'readinessSuccessThreshold')
if (!container.startupProbe) delete container.startupProbe
if (!container.livenessProbe) delete container.livenessProbe
if (!container.readinessProbe) delete container.readinessProbe
// Security Context (container-level)
if (config.enableSecurityContext) {
container.securityContext = {
runAsUser: config.runAsUser,
runAsGroup: config.runAsGroup,
runAsNonRoot: config.runAsNonRoot,
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
}
}
// 卷挂载
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)
}
const podSpec = { containers: [container] }
podSpec.restartPolicy = config.restartPolicy
if (config.nodeName) podSpec.nodeName = config.nodeName
// ServiceAccount
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
// 优雅终止
if (config.terminationGracePeriod) podSpec.terminationGracePeriodSeconds = config.terminationGracePeriod
// HostNetwork
if (config.hostNetwork) podSpec.hostNetwork = true
// DNS Policy
if (config.dnsPolicy && config.dnsPolicy !== 'ClusterFirst') podSpec.dnsPolicy = config.dnsPolicy
// NodeSelector
if (config.enableNodeSelector && config.nodeSelectorKey) {
podSpec.nodeSelector = { [config.nodeSelectorKey]: config.nodeSelectorValue }
}
// Affinity
const affinity = buildAffinity(config)
if (affinity) podSpec.affinity = affinity
// Tolerations
if (config.enableToleration) {
const t = { key: config.tolerationKey, operator: config.tolerationOperator, effect: config.tolerationEffect }
if (config.tolerationOperator === 'Equal') t.value = config.tolerationValue
if (config.tolerationEffect === 'NoExecute' && config.tolerationSeconds) t.tolerationSeconds = config.tolerationSeconds
podSpec.tolerations = [t]
}
// Volumes
if (volumes.length) podSpec.volumes = volumes
// fsGroup
if (config.enableSecurityContext && config.fsGroup) {
podSpec.securityContext = { fsGroup: config.fsGroup }
}
const pod = {
apiVersion: 'v1',
kind: 'Pod',
metadata: { name: config.name, namespace: config.namespace, labels },
spec: podSpec,
}
const header = `# K8s Pod - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'pod.yaml'}\n`
return header + '\n' + toYaml(pod)
}
function toYaml(obj, indent = 0) { function toYaml(obj, indent = 0) {
const lines = [] const lines = []
const prefix = ' '.repeat(indent) const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined || value === '') continue if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) { if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`) lines.push(`${prefix}${key}:`)
for (const item of value) { for (const item of value) {
@@ -228,9 +457,18 @@ function toYaml(obj, indent = 0) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue if (entries.length === 0) continue
const first = entries[0] const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`) 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++) { for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) 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 { } else {
lines.push(`${prefix} - ${item}`) lines.push(`${prefix} - ${item}`)
@@ -243,62 +481,5 @@ function toYaml(obj, indent = 0) {
lines.push(`${prefix}${key}: ${value}`) lines.push(`${prefix}${key}: ${value}`)
} }
} }
return lines.join('\n') return lines.join('\n')
} }
export function generateK8sPodYaml(config) {
const labels = {
app: config.appLabel || config.name,
}
if (config.versionLabel) {
labels.version = config.versionLabel
}
const container = {
name: config.containerName,
image: config.image,
imagePullPolicy: config.imagePullPolicy,
ports: [{
containerPort: config.containerPort,
protocol: config.protocol,
}],
resources: {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
},
}
if (config.command) {
container.command = config.command.split(',').map(s => s.trim())
}
if (config.args) {
container.args = config.args.split(',').map(s => s.trim())
}
if (config.envVars && config.envVars.length > 0) {
container.env = config.envVars.map(e => ({ name: e.key, value: e.value }))
}
const podSpec = {
containers: [container],
restartPolicy: config.restartPolicy,
}
if (config.nodeName) {
podSpec.nodeName = config.nodeName
}
const pod = {
apiVersion: 'v1',
kind: 'Pod',
metadata: {
name: config.name,
namespace: config.namespace,
labels,
},
spec: podSpec,
}
const header = `# K8s Pod - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'pod.yaml'}\n`
return header + '\n' + toYaml(pod)
}
+370 -203
View File
@@ -10,242 +10,348 @@ export const k8sStatefulSetSchema = {
{ {
title: '基础信息', title: '基础信息',
fields: [ fields: [
{ { key: 'name', label: 'StatefulSet 名称', type: 'text', placeholder: 'my-statefulset', default: 'my-statefulset', required: true },
key: 'name', { key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' },
label: 'StatefulSet 名称', { key: 'appName', label: '应用名称', type: 'text', placeholder: 'my-app', default: 'my-app', required: true, tip: '用作 selector 和 label 的 app 值' },
type: 'text', { key: 'image', label: '容器镜像', type: 'text', placeholder: 'nginx:1.24', default: 'nginx:1.24', required: true },
placeholder: 'my-statefulset', { key: 'replicas', label: '副本数', type: 'number', min: 1, max: 100, default: 3 },
default: 'my-statefulset', { key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
required: true, { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
}, { label: 'Always (每次拉取)', value: 'Always' },
{ { label: 'Never (仅本地)', value: 'Never' },
key: 'namespace', ], default: 'IfNotPresent' },
label: '命名空间',
type: 'text',
placeholder: 'default',
default: 'default',
},
{
key: 'appName',
label: '应用名称',
type: 'text',
placeholder: 'my-app',
default: 'my-app',
required: true,
tip: '用作 selector 和 label 的 app 值',
},
{
key: 'image',
label: '容器镜像',
type: 'text',
placeholder: 'nginx:1.24',
default: 'nginx:1.24',
required: true,
},
{
key: 'replicas',
label: '副本数',
type: 'number',
min: 1,
max: 100,
default: 3,
},
], ],
}, },
{ {
title: '网络与服务', title: '网络与服务',
fields: [ fields: [
{ { key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
key: 'containerPort', { key: 'portName', label: '端口名称', type: 'text', default: 'http' },
label: '容器端口', { key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
type: 'number', { key: 'serviceName', label: 'Headless Service 名称', type: 'text', placeholder: 'my-service', default: 'my-service', required: true, tip: '必须已存在的 Headless Service' },
min: 1,
max: 65535,
default: 80,
},
{
key: 'serviceName',
label: 'Headless Service 名称',
type: 'text',
placeholder: 'my-service',
default: 'my-service',
required: true,
tip: '必须已存在的 Headless Service',
},
], ],
}, },
{ {
title: '持久存储', title: '持久存储',
fields: [ fields: [
{ { key: 'volumeClaimEnabled', label: '启用 PVC', type: 'switch', default: true, tip: '为每个 Pod 创建独立的 PersistentVolumeClaim' },
key: 'volumeClaimEnabled', { key: 'storageClassName', label: 'StorageClass', type: 'text', placeholder: 'standard', default: 'standard', dependsOn: { key: 'volumeClaimEnabled', value: true } },
label: '启用 PVC', { key: 'storageSize', label: '存储大小', type: 'text', placeholder: '10Gi', default: '10Gi', dependsOn: { key: 'volumeClaimEnabled', value: true } },
type: 'switch', { key: 'pvcAccessMode', label: '访问模式', type: 'select', options: [
default: true, { label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
tip: '为每个 Pod 创建独立的 PersistentVolumeClaim', { label: 'ReadWriteMany', value: 'ReadWriteMany' },
}, { label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
{ ], default: 'ReadWriteOnce', dependsOn: { key: 'volumeClaimEnabled', value: true } },
key: 'storageClassName',
label: 'StorageClass',
type: 'text',
placeholder: 'standard',
default: 'standard',
dependsOn: { key: 'volumeClaimEnabled', value: true },
},
{
key: 'storageSize',
label: '存储大小',
type: 'text',
placeholder: '10Gi',
default: '10Gi',
dependsOn: { key: 'volumeClaimEnabled', value: true },
},
{
key: 'pvcAccessMode',
label: '访问模式',
type: 'select',
options: [
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
],
default: 'ReadWriteOnce',
dependsOn: { key: 'volumeClaimEnabled', value: true },
},
], ],
}, },
{ {
title: '资源限制', title: '资源限制',
fields: [ fields: [
{ { key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
key: 'cpuRequest', { label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
label: 'CPU Requests', { label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
type: 'select', ], default: '100m' },
options: [ { key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
{ label: '50m', value: '50m' }, { label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
{ label: '100m', value: '100m' }, { label: '2', value: '2' },{ label: '4', value: '4' },
{ label: '200m', value: '200m' }, ], default: '500m' },
{ label: '500m', value: '500m' }, { key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
{ label: '1', value: '1' }, { label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
{ label: '2', value: '2' }, { label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
], ], default: '128Mi' },
default: '100m', { 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' },
key: 'cpuLimit', ], default: '256Mi' },
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' },
],
default: '256Mi',
},
], ],
}, },
{ {
title: '更新策略', title: '更新策略',
fields: [ fields: [
{ { key: 'updateStrategy', label: '更新策略', type: 'select', options: [
key: 'updateStrategy', { label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
label: '更新策略', { label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
type: 'select', ], default: 'RollingUpdate' },
options: [ { key: 'partition', label: 'Partition', type: 'number', min: 0, max: 100, default: 0, tip: '仅更新序号 >= partition 的 Pod', dependsOn: { key: 'updateStrategy', value: 'RollingUpdate' } },
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' }, ],
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' }, },
], {
default: 'RollingUpdate', title: '启动探针 (Startup Probe)',
}, fields: [
{ { key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
key: 'partition', { key: 'startupProbeType', label: '探针方式', type: 'select', options: [
label: 'Partition', { label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
type: 'number', ], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
min: 0, { key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
max: 100, { key: 'startupPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableStartupProbe', value: true } },
default: 0, { key: 'startupCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'startupProbeType', value: 'exec' } },
tip: '仅更新序号 >= partition 的 Pod', { key: 'startupInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 0, dependsOn: { key: 'enableStartupProbe', value: true } },
dependsOn: { key: 'updateStrategy', value: 'RollingUpdate' }, { key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
}, { key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
],
},
{
title: '存活探针 (Liveness Probe)',
fields: [
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
{ key: 'livenessProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 20, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableLivenessProbe', value: true } },
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
],
},
{
title: '就绪探针 (Readiness Probe)',
fields: [
{ key: 'enableReadinessProbe', label: '启用就绪探针', type: 'switch', default: true, tip: '失败则从 Service 端点摘除' },
{ key: 'readinessProbeType', label: '探针方式', type: 'select', options: [
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
{ key: 'readinessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
{ key: 'readinessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 5, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', value: true } },
],
},
{
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: '节点调度 — NodeSelector',
fields: [
{ key: 'enableNodeSelector', label: '启用 NodeSelector', type: 'switch', default: false, tip: '简单的节点标签匹配' },
{ key: 'nodeSelectorKey', label: '标签 Key', type: 'text', placeholder: 'kubernetes.io/os', default: 'kubernetes.io/os', dependsOn: { key: 'enableNodeSelector', value: true } },
{ key: 'nodeSelectorValue', label: '标签 Value', type: 'text', placeholder: 'linux', default: 'linux', dependsOn: { key: 'enableNodeSelector', value: true } },
],
},
{
title: '节点调度 — 节点亲和性 (Node Affinity)',
fields: [
{ key: 'enableNodeAffinity', label: '启用节点亲和性', type: 'switch', default: false, tip: '比 NodeSelector 更灵活的调度规则' },
{ key: 'nodeAffinityType', label: '亲和类型', type: 'select', options: [
{ label: 'required - 必须满足 (硬性)', value: 'required' },
{ label: 'preferred - 尽量满足 (软性)', value: 'preferred' },
], default: 'required', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityKey1', label: '匹配标签 Key', type: 'text', placeholder: 'topology.kubernetes.io/zone', default: 'topology.kubernetes.io/zone', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityOperator1', label: '操作符', type: 'select', options: [
{ label: 'In - 值在列表中', value: 'In' },
{ label: 'NotIn - 值不在列表中', value: 'NotIn' },
{ label: 'Exists - 标签存在', value: 'Exists' },
{ label: 'DoesNotExist - 标签不存在', value: 'DoesNotExist' },
{ label: 'Gt - 大于', value: 'Gt' },
{ label: 'Lt - 小于', value: 'Lt' },
], default: 'In', dependsOn: { key: 'enableNodeAffinity', value: true } },
{ key: 'nodeAffinityValues1', label: '匹配值 (逗号分隔)', type: 'text', placeholder: 'cn-east-1a,cn-east-1b', default: 'cn-east-1a,cn-east-1b', dependsOn: { key: 'nodeAffinityOperator1', valueNotIn: ['Exists', 'DoesNotExist'] } },
{ key: 'nodeAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 80, dependsOn: { key: 'nodeAffinityType', value: 'preferred' } },
],
},
{
title: 'Pod 反亲和性 (Pod Anti-Affinity)',
fields: [
{ key: 'enablePodAntiAffinity', label: '启用 Pod 反亲和性', type: 'switch', default: false, tip: '让副本分散到不同节点/区域,提高可用性' },
{ key: 'podAntiAffinityType', label: '亲和类型', type: 'select', options: [
{ label: 'required - 必须分散 (硬性)', value: 'required' },
{ label: 'preferred - 尽量分散 (软性)', value: 'preferred' },
], default: 'preferred', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
{ key: 'podAntiAffinityKey', label: '匹配标签 Key', type: 'text', default: 'app', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
{ key: 'podAntiAffinityTopology', label: '拓扑域', type: 'select', options: [
{ label: 'kubernetes.io/hostname (节点级)', value: 'kubernetes.io/hostname' },
{ label: 'topology.kubernetes.io/zone (可用区级)', value: 'topology.kubernetes.io/zone' },
{ label: 'topology.kubernetes.io/region (地域级)', value: 'topology.kubernetes.io/region' },
], default: 'kubernetes.io/hostname', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
{ key: 'podAntiAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 100, dependsOn: { key: 'podAntiAffinityType', value: 'preferred' } },
],
},
{
title: '容忍度 (Tolerations)',
fields: [
{ key: 'enableToleration', label: '启用容忍度', type: 'switch', default: false, tip: '允许调度到有 taint 的节点' },
{ key: 'tolerationKey', label: 'Taint Key', type: 'text', placeholder: 'node-role.kubernetes.io/master', default: 'node-role.kubernetes.io/master', dependsOn: { key: 'enableToleration', value: true } },
{ key: 'tolerationOperator', label: '操作符', type: 'select', options: [
{ label: 'Equal - 值匹配', value: 'Equal' },
{ label: 'Exists - Key 存在即可', value: 'Exists' },
], default: 'Exists', dependsOn: { key: 'enableToleration', value: true } },
{ key: 'tolerationValue', label: 'Taint Value', type: 'text', default: '', dependsOn: { key: 'tolerationOperator', value: 'Equal' } },
{ key: 'tolerationEffect', label: 'Taint Effect', type: 'select', options: [
{ label: 'NoSchedule - 不调度新 Pod', value: 'NoSchedule' },
{ label: 'PreferNoSchedule - 尽量不调度', value: 'PreferNoSchedule' },
{ label: 'NoExecute - 驱逐已有 Pod', value: 'NoExecute' },
], default: 'NoSchedule', dependsOn: { key: 'enableToleration', value: true } },
{ key: 'tolerationSeconds', label: '容忍时长 (秒)', type: 'number', min: 0, max: 86400, default: 300, dependsOn: { key: 'tolerationEffect', value: 'NoExecute' }, tip: '仅 NoExecute 有效' },
],
},
{
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: '标签与注解',
fields: [
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用应用名称' },
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
], ],
}, },
], ],
} }
function toYaml(obj, indent = 0) { // ======================== YAML 生成器 ========================
const lines = []
const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) { function buildProbe(config, prefix, type, path, port, cmd, headers, initDelay, period, timeout, failure, success) {
if (value === null || value === undefined || value === '') continue if (!config[prefix]) return null
const probeType = config[type] || 'httpGet'
const probe = {}
if (Array.isArray(value)) { if (probeType === 'httpGet') {
lines.push(`${prefix}${key}:`) probe.httpGet = { path: config[path], port: config[port], scheme: 'HTTP' }
for (const item of value) { const h = config[headers]
if (typeof item === 'object' && item !== null) { if (h) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') const hdrs = h.split('\n').filter(Boolean).map(line => {
if (entries.length === 0) continue const [k, ...v] = line.split(':')
const first = entries[0] return { name: k.trim(), value: v.join(':').trim() }
lines.push(`${prefix} - ${first[0]}: ${first[1]}`) })
for (let i = 1; i < entries.length; i++) { if (hdrs.length) probe.httpGet.httpHeaders = hdrs
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) }
} } else if (probeType === 'tcpSocket') {
} else { probe.tcpSocket = { port: config[port] }
lines.push(`${prefix} - ${item}`) } else {
} const c = config[cmd] || 'cat /tmp/healthy'
} probe.exec = { command: c.split(' ') }
} else if (typeof value === 'object') { }
lines.push(`${prefix}${key}:`)
lines.push(toYaml(value, indent + 1)) probe.initialDelaySeconds = config[initDelay]
probe.periodSeconds = config[period]
probe.timeoutSeconds = config[timeout]
probe.failureThreshold = config[failure]
if (success !== null) probe.successThreshold = config[success]
return probe
}
function buildAffinity(config) {
const affinity = {}
// Node Affinity
if (config.enableNodeAffinity) {
const key = config.nodeAffinityKey1
const op = config.nodeAffinityOperator1
const vals = (config.nodeAffinityValues1 || '').split(',').map(s => s.trim()).filter(Boolean)
const term = { matchExpressions: [{ key, operator: op }] }
if (['In', 'NotIn'].includes(op)) term.matchExpressions[0].values = vals
if (config.nodeAffinityType === 'required') {
affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [term] } }
} else { } else {
lines.push(`${prefix}${key}: ${value}`) affinity.nodeAffinity = {
preferredDuringSchedulingIgnoredDuringExecution: [{
weight: config.nodeAffinityWeight || 80,
preference: term,
}],
}
} }
} }
return lines.join('\n') // Pod Anti-Affinity
if (config.enablePodAntiAffinity) {
const labelKey = config.podAntiAffinityKey || 'app'
const topology = config.podAntiAffinityTopology || 'kubernetes.io/hostname'
const term = {
labelSelector: { matchExpressions: [{ key: labelKey, operator: 'In', values: [config.appLabel || config.appName] }] },
topologyKey: topology,
}
if (config.podAntiAffinityType === 'required') {
affinity.podAntiAffinity = { requiredDuringSchedulingIgnoredDuringExecution: [term] }
} else {
affinity.podAntiAffinity = {
preferredDuringSchedulingIgnoredDuringExecution: [{
weight: config.podAntiAffinityWeight || 100,
podAffinityTerm: term,
}],
}
}
}
return Object.keys(affinity).length ? affinity : undefined
} }
export function generateK8sStatefulSetYaml(config) { export function generateK8sStatefulSetYaml(config) {
const labels = { app: config.appName } const labels = { app: config.appLabel || config.appName }
if (config.versionLabel) labels.version = config.versionLabel
const container = { const container = {
name: config.appName, name: config.appName,
image: config.image, image: config.image,
ports: [{ containerPort: config.containerPort, name: 'http' }], imagePullPolicy: config.imagePullPolicy,
ports: [{ name: config.portName, containerPort: config.containerPort, protocol: config.protocol }],
resources: { resources: {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest }, requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit }, limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
}, },
} }
// 探针
container.startupProbe = buildProbe(config, 'enableStartupProbe', 'startupProbeType', 'startupPath', 'startupPort', 'startupCommand', null, 'startupInitialDelay', 'startupPeriod', 'startupTimeout', 'startupFailureThreshold', null)
container.livenessProbe = buildProbe(config, 'enableLivenessProbe', 'livenessProbeType', 'livenessPath', 'livenessPort', 'livenessCommand', 'livenessHttpHeaders', 'livenessInitialDelay', 'livenessPeriod', 'livenessTimeout', 'livenessFailureThreshold', 'livenessSuccessThreshold')
container.readinessProbe = buildProbe(config, 'enableReadinessProbe', 'readinessProbeType', 'readinessPath', 'readinessPort', 'readinessCommand', 'readinessHttpHeaders', 'readinessInitialDelay', 'readinessPeriod', 'readinessTimeout', 'readinessFailureThreshold', 'readinessSuccessThreshold')
if (!container.startupProbe) delete container.startupProbe
if (!container.livenessProbe) delete container.livenessProbe
if (!container.readinessProbe) delete container.readinessProbe
// 环境变量
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 } })
}
// Security Context (container-level)
if (config.enableSecurityContext) {
container.securityContext = {
runAsUser: config.runAsUser,
runAsGroup: config.runAsGroup,
runAsNonRoot: config.runAsNonRoot,
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
}
}
// Volume mounts for PVC
const volumeMounts = [] const volumeMounts = []
const volumeClaimTemplates = [] const volumeClaimTemplates = []
@@ -265,25 +371,48 @@ export function generateK8sStatefulSetYaml(config) {
container.volumeMounts = volumeMounts container.volumeMounts = volumeMounts
} }
const podSpec = { containers: [container] }
// ServiceAccount
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
// 优雅终止
if (config.terminationGracePeriod) podSpec.terminationGracePeriodSeconds = config.terminationGracePeriod
// NodeSelector
if (config.enableNodeSelector && config.nodeSelectorKey) {
podSpec.nodeSelector = { [config.nodeSelectorKey]: config.nodeSelectorValue }
}
// Affinity
const affinity = buildAffinity(config)
if (affinity) podSpec.affinity = affinity
// Tolerations
if (config.enableToleration) {
const t = { key: config.tolerationKey, operator: config.tolerationOperator, effect: config.tolerationEffect }
if (config.tolerationOperator === 'Equal') t.value = config.tolerationValue
if (config.tolerationEffect === 'NoExecute' && config.tolerationSeconds) t.tolerationSeconds = config.tolerationSeconds
podSpec.tolerations = [t]
}
// fsGroup
if (config.enableSecurityContext && config.fsGroup) {
podSpec.securityContext = { fsGroup: config.fsGroup }
}
const statefulset = { const statefulset = {
apiVersion: 'apps/v1', apiVersion: 'apps/v1',
kind: 'StatefulSet', kind: 'StatefulSet',
metadata: { metadata: { name: config.name, namespace: config.namespace, labels },
name: config.name,
namespace: config.namespace,
labels,
},
spec: { spec: {
replicas: config.replicas, replicas: config.replicas,
serviceName: config.serviceName, serviceName: config.serviceName,
selector: { matchLabels: { app: config.appName } }, selector: { matchLabels: { app: config.appLabel || config.appName } },
updateStrategy: config.updateStrategy === 'RollingUpdate' updateStrategy: config.updateStrategy === 'RollingUpdate'
? { type: 'RollingUpdate', rollingUpdate: { partition: config.partition } } ? { type: 'RollingUpdate', rollingUpdate: { partition: config.partition } }
: { type: 'OnDelete' }, : { type: 'OnDelete' },
template: { template: { metadata: { labels }, spec: podSpec },
metadata: { labels },
spec: { containers: [container] },
},
}, },
} }
@@ -292,6 +421,44 @@ export function generateK8sStatefulSetYaml(config) {
} }
const header = `# K8s StatefulSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'statefulset.yaml'}\n` const header = `# K8s StatefulSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'statefulset.yaml'}\n`
return header + '\n' + toYaml(statefulset) return header + '\n' + toYaml(statefulset)
} }
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')
}
+156 -6
View File
@@ -192,6 +192,105 @@ export const nginxSchema = {
}, },
], ],
}, },
{
title: '限流配置',
fields: [
{
key: 'enableLimitReq',
label: '启用请求限流',
type: 'switch',
default: false,
tip: '限制客户端请求速率,防止恶意请求',
},
{
key: 'limitReqZone',
label: '限流区域配置',
type: 'text',
default: "'$binary_remote_addr' zone=one:10m rate=10r/s",
tip: 'limit_zone 参数,如 $binary_remote_addr zone=one:10m rate=10r/s',
dependsOn: { key: 'enableLimitReq', value: true },
},
{
key: 'limitReqBurst',
label: '突发请求数',
type: 'number',
min: 0,
max: 1000,
default: 20,
tip: '允许超出 rate 的突发请求数(burst',
dependsOn: { key: 'enableLimitReq', value: true },
},
],
},
{
title: 'Upstream 负载均衡',
fields: [
{
key: 'enableUpstream',
label: '启用 Upstream',
type: 'switch',
default: false,
tip: '配置后端服务器负载均衡组',
},
{
key: 'upstreamName',
label: 'Upstream 名称',
type: 'text',
default: 'backend',
tip: 'upstream 块名称,用于 proxy_pass 引用',
dependsOn: { key: 'enableUpstream', value: true },
},
{
key: 'upstreamServers',
label: '后端服务器列表',
type: 'text',
default: "server 127.0.0.1:8080 weight=3\nserver 127.0.0.1:8081 weight=2",
tip: '每行一个 server 指令,支持 weight/down/backup 参数',
dependsOn: { key: 'enableUpstream', value: true },
},
{
key: 'upstreamMethod',
label: '负载均衡算法',
type: 'select',
options: [
{ label: 'round-robin - 轮询 (默认)', value: 'round-robin' },
{ label: 'least_conn - 最少连接', value: 'least_conn' },
{ label: 'ip_hash - IP 哈希', value: 'ip_hash' },
],
default: 'least_conn',
tip: '请求分发策略',
dependsOn: { key: 'enableUpstream', value: true },
},
],
},
{
title: '缓存配置',
fields: [
{
key: 'enableCache',
label: '启用代理缓存',
type: 'switch',
default: false,
tip: '缓存后端响应以提升性能',
},
{
key: 'cachePath',
label: '缓存路径',
type: 'text',
default: '/var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m',
tip: 'proxy_cache_path 参数',
dependsOn: { key: 'enableCache', value: true },
},
{
key: 'cacheBypass',
label: '缓存绕过条件',
type: 'text',
default: '$cookie_nocache',
tip: 'proxy_cache_bypass 条件,满足时跳过缓存',
dependsOn: { key: 'enableCache', value: true },
},
],
},
], ],
} }
@@ -244,11 +343,45 @@ export function generateNginxConf(config) {
lines.push(``) lines.push(``)
} }
lines.push(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`) // 限流配置
lines.push(` # upstream backend {`) if (config.enableLimitReq) {
lines.push(` # server 127.0.0.1:8080;`) lines.push(` # 限流配置`)
lines.push(` # }`) lines.push(` limit_req_zone ${config.limitReqZone};`)
lines.push(``) lines.push(``)
}
// Upstream 负载均衡
if (config.enableUpstream) {
lines.push(` # Upstream 负载均衡`)
if (config.upstreamMethod && config.upstreamMethod !== 'round-robin') {
lines.push(` upstream ${config.upstreamName} {`)
lines.push(` ${config.upstreamMethod};`)
} else {
lines.push(` upstream ${config.upstreamName} {`)
}
const servers = config.upstreamServers.split('\n').filter(s => s.trim())
servers.forEach(server => {
lines.push(` ${server.trim()}`)
})
lines.push(` }`)
lines.push(``)
}
// 缓存配置
if (config.enableCache) {
lines.push(` # 代理缓存配置`)
lines.push(` proxy_cache_path ${config.cachePath};`)
lines.push(``)
}
// Upstream fallback when not enabled
if (!config.enableUpstream) {
lines.push(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`)
lines.push(` # upstream backend {`)
lines.push(` # server 127.0.0.1:8080;`)
lines.push(` # }`)
lines.push(``)
}
lines.push(` server {`) lines.push(` server {`)
lines.push(` listen ${config.httpPort};`) lines.push(` listen ${config.httpPort};`)
@@ -308,10 +441,19 @@ export function generateNginxConf(config) {
lines.push(``) lines.push(``)
} }
// 限流应用到 location
lines.push(` location / {`) lines.push(` location / {`)
if (config.enableLimitReq) {
lines.push(` limit_req zone=one burst=${config.limitReqBurst} nodelay;`)
}
if (config.enableProxy) { if (config.enableProxy) {
lines.push(` proxy_pass ${config.proxyTarget};`) if (config.enableUpstream) {
lines.push(` proxy_pass http://${config.upstreamName};`)
} else {
lines.push(` proxy_pass ${config.proxyTarget};`)
}
lines.push(` proxy_http_version 1.1;`) lines.push(` proxy_http_version 1.1;`)
lines.push(` proxy_set_header Upgrade $http_upgrade;`) lines.push(` proxy_set_header Upgrade $http_upgrade;`)
lines.push(` proxy_set_header Connection 'upgrade';`) lines.push(` proxy_set_header Connection 'upgrade';`)
@@ -320,6 +462,14 @@ export function generateNginxConf(config) {
lines.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`) lines.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
lines.push(` proxy_set_header X-Forwarded-Proto $scheme;`) lines.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
lines.push(` proxy_cache_bypass $http_upgrade;`) lines.push(` proxy_cache_bypass $http_upgrade;`)
if (config.enableCache) {
lines.push(` proxy_cache my_cache;`)
lines.push(` proxy_cache_valid 200 302 10m;`)
lines.push(` proxy_cache_valid 404 1m;`)
lines.push(` proxy_cache_bypass ${config.cacheBypass};`)
lines.push(` add_header X-Cache-Status $upstream_cache_status;`)
}
} else { } else {
lines.push(` root /usr/share/nginx/html;`) lines.push(` root /usr/share/nginx/html;`)
lines.push(` index index.html index.htm;`) lines.push(` index index.html index.htm;`)
+134
View File
@@ -180,6 +180,114 @@ export const postgresqlSchema = {
}, },
], ],
}, },
{
title: '连接池',
fields: [
{
key: 'superuserReservedConnections',
label: '超级用户保留连接数',
type: 'number',
min: 0,
max: 100,
default: 3,
tip: '为超级用户保留的连接数,确保管理员总能连入',
},
{
key: 'idleInTransactionSessionTimeout',
label: '空闲事务超时 (毫秒)',
type: 'number',
min: 0,
max: 86400000,
default: 0,
tip: '0 = 禁用。空闲事务超过此时间将被终止,防止连接泄漏',
},
],
},
{
title: '流复制',
fields: [
{
key: 'enableReplication',
label: '启用流复制',
type: 'switch',
default: false,
tip: '配置此实例支持流复制(主库或备库)',
},
{
key: 'hotStandby',
label: '热备模式',
type: 'switch',
default: true,
tip: '备库是否允许只读查询',
dependsOn: { key: 'enableReplication', value: true },
},
{
key: 'synchronousCommit',
label: '同步提交模式',
type: 'select',
options: [
{ label: 'on - 等待 WAL 写入本地磁盘 (默认)', value: 'on' },
{ label: 'remote_write - 等待备库写入 WAL', value: 'remote_write' },
{ label: 'off - 异步提交,性能最高', value: 'off' },
],
default: 'on',
tip: '同步提交级别,影响数据安全与性能',
dependsOn: { key: 'enableReplication', value: true },
},
],
},
{
title: 'Autovacuum',
fields: [
{
key: 'enableAutovacuum',
label: '启用 Autovacuum',
type: 'switch',
default: true,
tip: '自动清理死元组并更新统计信息',
},
{
key: 'autovacuumMaxWorkers',
label: '最大工作进程数',
type: 'number',
min: 1,
max: 10,
default: 3,
tip: '并行执行 autovacuum 的最大进程数',
dependsOn: { key: 'enableAutovacuum', value: true },
},
{
key: 'autovacuumNaptime',
label: '休眠间隔 (秒)',
type: 'number',
min: 1,
max: 86400,
default: 60,
tip: 'autovacuum 守护进程两次运行之间的休眠时间',
dependsOn: { key: 'enableAutovacuum', value: true },
},
{
key: 'autovacuumVacuumThreshold',
label: 'VACUUM 阈值',
type: 'number',
min: 0,
max: 10000,
default: 50,
tip: '触发 VACUUM 的最小死元组数量',
dependsOn: { key: 'enableAutovacuum', value: true },
},
{
key: 'autovacuumAnalyzeThreshold',
label: 'ANALYZE 阈值',
type: 'number',
min: 0,
max: 10000,
default: 50,
tip: '触发 ANALYZE 的最小变更行数',
dependsOn: { key: 'enableAutovacuum', value: true },
},
],
},
], ],
} }
@@ -225,5 +333,31 @@ export function generatePostgresqlConf(config) {
lines.push(``) lines.push(``)
} }
// 连接池
lines.push(`# ======================== 连接池 ========================`)
lines.push(`superuser_reserved_connections = ${config.superuserReservedConnections}`)
if (config.idleInTransactionSessionTimeout > 0) {
lines.push(`idle_in_transaction_session_timeout = ${config.idleInTransactionSessionTimeout}`)
}
lines.push(``)
// 流复制
if (config.enableReplication) {
lines.push(`# ======================== 流复制 ========================`)
lines.push(`hot_standby = ${config.hotStandby ? 'on' : 'off'}`)
lines.push(`synchronous_commit = ${config.synchronousCommit}`)
lines.push(``)
}
// Autovacuum
lines.push(`# ======================== Autovacuum ========================`)
lines.push(`autovacuum = ${config.enableAutovacuum ? 'on' : 'off'}`)
if (config.enableAutovacuum) {
lines.push(`autovacuum_max_workers = ${config.autovacuumMaxWorkers}`)
lines.push(`autovacuum_naptime = ${config.autovacuumNaptime}`)
lines.push(`autovacuum_vacuum_threshold = ${config.autovacuumVacuumThreshold}`)
lines.push(`autovacuum_analyze_threshold = ${config.autovacuumAnalyzeThreshold}`)
}
return lines.join('\n') return lines.join('\n')
} }
+152
View File
@@ -204,6 +204,117 @@ export const redisSchema = {
}, },
], ],
}, },
{
title: 'ACL 配置',
fields: [
{
key: 'enableAcl',
label: '启用 ACL',
type: 'switch',
default: false,
tip: '启用 Redis ACL 访问控制列表',
},
{
key: 'aclFile',
label: 'ACL 文件路径',
type: 'text',
default: '/etc/redis/users.acl',
tip: 'ACL 规则文件路径',
dependsOn: { key: 'enableAcl', value: true },
},
{
key: 'defaultUserPassword',
label: '默认用户密码',
type: 'text',
placeholder: '留空表示无密码',
default: '',
tip: '设置 default 用户的密码',
dependsOn: { key: 'enableAcl', value: true },
},
{
key: 'enableRenameCommand',
label: '重命名危险命令',
type: 'switch',
default: false,
tip: '重命名 FLUSHALL/FLUSHDB/CONFIG 命令以增强安全性',
dependsOn: { key: 'enableAcl', value: true },
},
],
},
{
title: '慢日志',
fields: [
{
key: 'slowlogLogSlowerThan',
label: '慢日志阈值 (微秒)',
type: 'number',
min: 0,
max: 10000000,
default: 10000,
tip: '执行时间超过此值(微秒)的命令将被记录,0 表示记录所有',
},
{
key: 'slowlogMaxLen',
label: '慢日志最大条数',
type: 'number',
min: 1,
max: 10000,
default: 128,
tip: '慢日志队列最大长度,超出后旧记录被丢弃',
},
],
},
{
title: '延迟监控',
fields: [
{
key: 'latencyMonitorThreshold',
label: '延迟监控阈值 (毫秒)',
type: 'number',
min: 0,
max: 86400000,
default: 0,
tip: '0 = 禁用延迟监控,设置大于 0 的值启用(单位毫秒)',
},
],
},
{
title: '客户端管理',
fields: [
{
key: 'maxmemorySamples',
label: '内存采样数量',
type: 'number',
min: 1,
max: 10,
default: 5,
tip: 'LRU/TTL 淘汰算法的采样数,越大越精确但越慢',
},
{
key: 'lazyfreeLazyEviction',
label: '惰性释放内存淘汰',
type: 'switch',
default: false,
tip: '内存淘汰时异步释放键,避免阻塞主线程',
},
{
key: 'lazyfreeLazyExpire',
label: '惰性释放过期键',
type: 'switch',
default: true,
tip: '过期键异步释放,避免阻塞主线程',
},
{
key: 'ioThreads',
label: 'IO 线程数',
type: 'number',
min: 1,
max: 8,
default: 1,
tip: '1 表示禁用多线程 IO,建议不超过 CPU 核心数',
},
],
},
], ],
} }
@@ -314,6 +425,47 @@ export function generateRedisConf(config) {
lines.push(`tcp-keepalive ${config.tcpKeepalive}`) lines.push(`tcp-keepalive ${config.tcpKeepalive}`)
lines.push(`maxclients ${config.maxclients}`) lines.push(`maxclients ${config.maxclients}`)
lines.push(`tcp-backlog 511`) lines.push(`tcp-backlog 511`)
lines.push(``)
// ACL 配置
if (config.enableAcl) {
lines.push(`################################## ACL 配置 ##################################`)
lines.push(``)
lines.push(`aclfile ${config.aclFile}`)
if (config.defaultUserPassword) {
lines.push(`# 默认用户密码已设置`)
lines.push(`acl setuser default >${config.defaultUserPassword} ~* +@all`)
}
if (config.enableRenameCommand) {
lines.push(``)
lines.push(`# 重命名危险命令`)
lines.push(`rename-command FLUSHALL ""`)
lines.push(`rename-command FLUSHDB ""`)
lines.push(`rename-command CONFIG ""`)
}
lines.push(``)
}
// 慢日志
lines.push(`################################## 慢日志 ##################################`)
lines.push(``)
lines.push(`slowlog-log-slower-than ${config.slowlogLogSlowerThan}`)
lines.push(`slowlog-max-len ${config.slowlogMaxLen}`)
lines.push(``)
// 延迟监控
lines.push(`################################## 延迟监控 ##################################`)
lines.push(``)
lines.push(`latency-monitor-threshold ${config.latencyMonitorThreshold}`)
lines.push(``)
// 客户端管理
lines.push(`################################## 客户端管理 ##################################`)
lines.push(``)
lines.push(`maxmemory-samples ${config.maxmemorySamples}`)
lines.push(`lazyfree-lazy-eviction ${config.lazyfreeLazyEviction ? 'yes' : 'no'}`)
lines.push(`lazyfree-lazy-expire ${config.lazyfreeLazyExpire ? 'yes' : 'no'}`)
lines.push(`io-threads ${config.ioThreads}`)
return lines.join('\n') return lines.join('\n')
} }