diff --git a/src/schemas/k8s-cronjob.js b/src/schemas/k8s-cronjob.js index bca9a47..20953d5 100644 --- a/src/schemas/k8s-cronjob.js +++ b/src/schemas/k8s-cronjob.js @@ -1,9 +1,14 @@ +/** + * K8s CronJob 生产级 Schema — 由 ConfTemplate 生成 + * 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount + */ + export const k8sCronJobSchema = { id: 'k8s-cronjob', name: 'K8s CronJob', icon: 'Box', category: 'K8S 工作负载', - description: 'Kubernetes CronJob — 定时调度批处理任务', + description: 'Kubernetes CronJob — 定时调度批处理任务(生产级)', format: 'yaml', fileName: 'cronjob.yaml', groups: [ @@ -33,6 +38,17 @@ export const k8sCronJobSchema = { default: 'busybox:latest', required: true, }, + { + key: 'imagePullPolicy', + label: '镜像拉取策略', + type: 'select', + options: [ + { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' }, + { label: 'Always (每次拉取)', value: 'Always' }, + { label: 'Never (仅本地)', value: 'Never' }, + ], + default: 'IfNotPresent', + }, { key: 'command', label: 'Command', @@ -118,6 +134,102 @@ export const k8sCronJobSchema = { max: 100, default: 6, }, + { + key: 'activeDeadlineSeconds', + label: '超时时间 (秒)', + type: 'number', + min: 0, + max: 86400, + default: 0, + tip: '0 表示不限制超时', + }, + { + key: 'ttlSecondsAfterFinished', + label: '自动清理 (秒)', + type: 'number', + min: 0, + max: 86400, + default: 0, + tip: '完成后自动删除 Job,0 表示保留', + }, + ], + }, + { + title: '容器端口', + fields: [ + { key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 }, + { key: 'portName', label: '端口名称', type: 'text', default: 'http' }, + { key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' }, + ], + }, + { + title: '资源限制', + fields: [ + { key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [ + { label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' }, + { label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' }, + ], default: '100m' }, + { key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [ + { label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' }, + { label: '2', value: '2' },{ label: '4', value: '4' }, + ], default: '500m' }, + { key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [ + { label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' }, + { label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' }, + ], default: '128Mi' }, + { key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [ + { label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' }, + { label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },{ label: '4Gi', value: '4Gi' }, + ], default: '256Mi' }, + ], + }, + { + title: '环境变量', + fields: [ + { key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false }, + { key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'APP_ENV=production\nLOG_LEVEL=info', default: 'APP_ENV=production\nLOG_LEVEL=info', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' }, + { key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false }, + { key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } }, + { key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false }, + { key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } }, + ], + }, + { + title: '卷挂载', + fields: [ + { key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false }, + { key: 'volumeType', label: '卷类型', type: 'select', options: [ + { label: 'emptyDir - 临时目录', value: 'emptyDir' }, + { label: 'PersistentVolumeClaim', value: 'pvc' }, + { label: 'ConfigMap', value: 'configMap' }, + { label: 'Secret', value: 'secret' }, + { label: 'HostPath - 主机路径', value: 'hostPath' }, + ], default: 'emptyDir', dependsOn: { key: 'enableVolumeMount', value: true } }, + { key: 'volumeName', label: '卷名称', type: 'text', default: 'data', dependsOn: { key: 'enableVolumeMount', value: true } }, + { key: 'volumeMountPath', label: '挂载路径', type: 'text', default: '/data', dependsOn: { key: 'enableVolumeMount', value: true } }, + { key: 'volumeReadOnly', label: '只读挂载', type: 'switch', default: false, dependsOn: { key: 'enableVolumeMount', value: true } }, + { key: 'pvcClaimName', label: 'PVC 名称', type: 'text', default: 'my-pvc', dependsOn: { key: 'volumeType', value: 'pvc' } }, + { key: 'configMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'volumeType', value: 'configMap' } }, + { key: 'secretVolumeName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'volumeType', value: 'secret' } }, + { key: 'hostPath', label: '主机路径', type: 'text', default: '/tmp/data', dependsOn: { key: 'volumeType', value: 'hostPath' } }, + ], + }, + { + title: '安全上下文 (Security Context)', + fields: [ + { key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false }, + { key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } }, + { key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } }, + { key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } }, + { key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' }, + { key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } }, + { key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' }, + ], + }, + { + title: 'ServiceAccount', + fields: [ + { key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' }, ], }, ], @@ -126,10 +238,8 @@ export const k8sCronJobSchema = { 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) { @@ -137,9 +247,18 @@ function toYaml(obj, indent = 0) { const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') if (entries.length === 0) continue const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) + 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++) { - 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 { lines.push(`${prefix} - ${item}`) @@ -152,7 +271,6 @@ function toYaml(obj, indent = 0) { lines.push(`${prefix}${key}: ${value}`) } } - return lines.join('\n') } @@ -160,9 +278,79 @@ export function generateK8sCronJobYaml(config) { const container = { name: config.name, image: config.image, + imagePullPolicy: config.imagePullPolicy, command: config.command.split(' '), } + // 容器端口 + if (config.containerPort) { + container.ports = [{ + name: config.portName || 'http', + containerPort: config.containerPort, + protocol: config.protocol || 'TCP', + }] + } + + // 资源限制 + container.resources = { + requests: { cpu: config.cpuRequest, memory: config.memoryRequest }, + limits: { cpu: config.cpuLimit, memory: config.memoryLimit }, + } + + // 环境变量 + if (config.enableEnv && config.envVars) { + container.env = config.envVars.split('\n').filter(Boolean).map(line => { + const [k, ...v] = line.split('=') + return { name: k.trim(), value: v.join('=').trim() } + }) + } + if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) { + container.envFrom = [] + if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } }) + if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } }) + } + + // 卷挂载 + const volumes = [] + if (config.enableVolumeMount) { + container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }] + const vol = { name: config.volumeName } + const vt = config.volumeType + if (vt === 'emptyDir') vol.emptyDir = {} + else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName } + else if (vt === 'configMap') vol.configMap = { name: config.configMapName } + else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName } + else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' } + volumes.push(vol) + } + + // Security Context + if (config.enableSecurityContext) { + container.securityContext = { + runAsUser: config.runAsUser, + runAsGroup: config.runAsGroup, + runAsNonRoot: config.runAsNonRoot, + readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined, + allowPrivilegeEscalation: config.allowPrivilegeEscalation, + } + } + + const podSpec = { + containers: [container], + restartPolicy: config.restartPolicy, + } + + // ServiceAccount + if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName + + // Volumes + if (volumes.length) podSpec.volumes = volumes + + // fsGroup + if (config.enableSecurityContext && config.fsGroup) { + podSpec.securityContext = { fsGroup: config.fsGroup } + } + const cronjob = { apiVersion: 'batch/v1', kind: 'CronJob', @@ -179,11 +367,10 @@ export function generateK8sCronJobYaml(config) { jobTemplate: { spec: { backoffLimit: config.backoffLimit, + activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined, + ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined, template: { - spec: { - containers: [container], - restartPolicy: config.restartPolicy, - }, + spec: podSpec, }, }, }, diff --git a/src/schemas/k8s-daemonset.js b/src/schemas/k8s-daemonset.js index 452c2de..3bbf253 100644 --- a/src/schemas/k8s-daemonset.js +++ b/src/schemas/k8s-daemonset.js @@ -10,161 +10,358 @@ export const k8sDaemonSetSchema = { { title: '基础信息', fields: [ - { - key: 'name', - label: 'DaemonSet 名称', - type: 'text', - placeholder: 'my-daemonset', - default: 'my-daemonset', - required: true, - }, - { - key: 'namespace', - label: '命名空间', - type: 'text', - placeholder: 'default', - default: 'default', - }, - { - key: 'appName', - 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, - }, + { key: 'name', label: 'DaemonSet 名称', type: 'text', placeholder: 'my-daemonset', default: 'my-daemonset', required: true }, + { key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' }, + { key: 'appName', 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: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [ + { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' }, + { label: 'Always (每次拉取)', value: 'Always' }, + { label: 'Never (仅本地)', value: 'Never' }, + ], default: 'IfNotPresent' }, + ], + }, + { + title: '容器端口', + fields: [ + { key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 8080 }, + { 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' }, - ], - 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' }, - ], - 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', - }, + { 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' }, + ], 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' }, + ], 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: '节点调度', fields: [ - { - key: 'hostNetwork', - label: '使用主机网络', - type: 'switch', - 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 }, - }, + { key: 'hostNetwork', label: '使用主机网络', type: 'switch', 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: '更新策略', fields: [ - { - key: 'updateStrategy', - label: '更新策略', - type: 'select', - options: [ - { label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' }, - { label: 'OnDelete - 手动删除触发', value: 'OnDelete' }, - ], - default: 'RollingUpdate', - }, + { key: 'updateStrategy', label: '更新策略', type: 'select', options: [ + { 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: '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) { 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) { @@ -172,9 +369,18 @@ function toYaml(obj, indent = 0) { const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') if (entries.length === 0) continue const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) + 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++) { - 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 { lines.push(`${prefix} - ${item}`) @@ -187,61 +393,5 @@ function toYaml(obj, indent = 0) { lines.push(`${prefix}${key}: ${value}`) } } - 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) -} diff --git a/src/schemas/k8s-job.js b/src/schemas/k8s-job.js index 434c00c..541b5c6 100644 --- a/src/schemas/k8s-job.js +++ b/src/schemas/k8s-job.js @@ -1,9 +1,14 @@ +/** + * K8s Job 生产级 Schema — 由 ConfTemplate 生成 + * 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount + */ + export const k8sJobSchema = { id: 'k8s-job', name: 'K8s Job', icon: 'Box', category: 'K8S 工作负载', - description: 'Kubernetes Job — 运行一次性批处理任务', + description: 'Kubernetes Job — 运行一次性批处理任务(生产级)', format: 'yaml', fileName: 'job.yaml', groups: [ @@ -33,6 +38,17 @@ export const k8sJobSchema = { default: 'busybox:latest', required: true, }, + { + key: 'imagePullPolicy', + label: '镜像拉取策略', + type: 'select', + options: [ + { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' }, + { label: 'Always (每次拉取)', value: 'Always' }, + { label: 'Never (仅本地)', value: 'Never' }, + ], + default: 'IfNotPresent', + }, { key: 'command', label: 'Command', @@ -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) { 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) { @@ -126,9 +218,18 @@ function toYaml(obj, indent = 0) { const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') if (entries.length === 0) continue const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) + 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++) { - 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 { lines.push(`${prefix} - ${item}`) @@ -141,7 +242,6 @@ function toYaml(obj, indent = 0) { lines.push(`${prefix}${key}: ${value}`) } } - return lines.join('\n') } @@ -149,9 +249,79 @@ export function generateK8sJobYaml(config) { const container = { name: config.name, image: config.image, + imagePullPolicy: config.imagePullPolicy, command: config.command.split(' '), } + // 容器端口 + if (config.containerPort) { + container.ports = [{ + name: config.portName || 'http', + containerPort: config.containerPort, + protocol: config.protocol || 'TCP', + }] + } + + // 资源限制 + container.resources = { + requests: { cpu: config.cpuRequest, memory: config.memoryRequest }, + limits: { cpu: config.cpuLimit, memory: config.memoryLimit }, + } + + // 环境变量 + if (config.enableEnv && config.envVars) { + container.env = config.envVars.split('\n').filter(Boolean).map(line => { + const [k, ...v] = line.split('=') + return { name: k.trim(), value: v.join('=').trim() } + }) + } + if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) { + container.envFrom = [] + if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } }) + if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } }) + } + + // 卷挂载 + const volumes = [] + if (config.enableVolumeMount) { + container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }] + const vol = { name: config.volumeName } + const vt = config.volumeType + if (vt === 'emptyDir') vol.emptyDir = {} + else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName } + else if (vt === 'configMap') vol.configMap = { name: config.configMapName } + else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName } + else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' } + volumes.push(vol) + } + + // Security Context + if (config.enableSecurityContext) { + container.securityContext = { + runAsUser: config.runAsUser, + runAsGroup: config.runAsGroup, + runAsNonRoot: config.runAsNonRoot, + readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined, + allowPrivilegeEscalation: config.allowPrivilegeEscalation, + } + } + + const podSpec = { + containers: [container], + restartPolicy: config.restartPolicy, + } + + // ServiceAccount + if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName + + // Volumes + if (volumes.length) podSpec.volumes = volumes + + // fsGroup + if (config.enableSecurityContext && config.fsGroup) { + podSpec.securityContext = { fsGroup: config.fsGroup } + } + const job = { apiVersion: 'batch/v1', kind: 'Job', @@ -163,23 +333,14 @@ export function generateK8sJobYaml(config) { backoffLimit: config.backoffLimit, completions: config.completions, parallelism: config.parallelism, + activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined, + ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined, template: { - spec: { - containers: [container], - restartPolicy: config.restartPolicy, - }, + spec: podSpec, }, }, } - 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` return header + '\n' + toYaml(job) diff --git a/src/schemas/k8s-pod.js b/src/schemas/k8s-pod.js index 7e47592..5d14973 100644 --- a/src/schemas/k8s-pod.js +++ b/src/schemas/k8s-pod.js @@ -10,217 +10,446 @@ export const k8sPodSchema = { { title: '基础信息', fields: [ - { - key: 'name', - label: 'Pod 名称', - type: 'text', - placeholder: 'my-pod', - default: 'my-pod', - required: true, - }, - { - 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', - }, + { key: 'name', label: 'Pod 名称', type: 'text', placeholder: 'my-pod', default: 'my-pod', required: true }, + { 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: '容器端口', fields: [ - { - key: 'containerPort', - label: '容器端口', - type: 'number', - min: 1, - max: 65535, - default: 80, - }, - { - key: 'protocol', - label: '协议', - type: 'select', - options: [ - { label: 'TCP', value: 'TCP' }, - { label: 'UDP', value: 'UDP' }, - ], - default: 'TCP', - }, + { 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: 'command', - label: 'Command', - type: 'text', - placeholder: '/bin/sh', - default: '', - tip: '容器 ENTRYPOINT,留空则使用镜像默认值', - }, - { - key: 'args', - label: 'Args', - type: 'text', - placeholder: '-c, echo hello', - default: '', - tip: '逗号分隔多个参数', - }, + { key: 'command', label: 'Command', type: 'text', placeholder: '/bin/sh', default: '', tip: '容器 ENTRYPOINT,留空则使用镜像默认值' }, + { key: 'args', label: 'Args', type: 'text', placeholder: '-c, echo hello', default: '', tip: '逗号分隔多个参数' }, ], }, { title: '环境变量 (可选)', fields: [ - { - key: 'envVars', - label: '环境变量', - type: 'keyvalue', - default: [], - tip: '键值对,将注入为容器环境变量', - }, + { key: 'envVars', label: '环境变量', type: 'keyvalue', default: [], tip: '键值对,将注入为容器环境变量' }, + { 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: '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' }, - ], - default: '256Mi', - }, + { 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' }, + ], default: '256Mi' }, ], }, { title: '重启与调度', fields: [ - { - key: 'restartPolicy', - label: '重启策略', - type: 'select', - options: [ - { label: 'Always (默认)', value: 'Always' }, - { label: 'OnFailure', value: 'OnFailure' }, - { label: 'Never', value: 'Never' }, - ], - default: 'Always', - }, - { - key: 'nodeName', - label: '指定节点', - type: 'text', - placeholder: 'node-1', - default: '', - tip: '留空则由调度器自动分配', - }, + { key: 'restartPolicy', label: '重启策略', type: 'select', options: [ + { label: 'Always (默认)', value: 'Always' }, + { label: 'OnFailure', value: 'OnFailure' }, + { label: 'Never', value: 'Never' }, + ], default: 'Always' }, + { key: 'nodeName', label: '指定节点', type: 'text', placeholder: 'node-1', default: '', tip: '留空则由调度器自动分配' }, ], }, { title: '标签', fields: [ - { - key: 'appLabel', - label: 'app 标签值', - type: 'text', - default: '', - tip: '留空则使用 Pod 名称', - }, - { - key: 'versionLabel', - label: 'version 标签', - type: 'text', - placeholder: 'v1', - default: 'v1', - }, + { key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用 Pod 名称' }, + { key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' }, + ], + }, + { + title: '启动探针 (Startup Probe)', + 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: 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) { 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) { @@ -228,9 +457,18 @@ function toYaml(obj, indent = 0) { const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '') if (entries.length === 0) continue const first = entries[0] - lines.push(`${prefix} - ${first[0]}: ${first[1]}`) + 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++) { - 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 { lines.push(`${prefix} - ${item}`) @@ -243,62 +481,5 @@ function toYaml(obj, indent = 0) { lines.push(`${prefix}${key}: ${value}`) } } - 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) -} diff --git a/src/schemas/k8s-statefulset.js b/src/schemas/k8s-statefulset.js index b9d5d7d..7680f2e 100644 --- a/src/schemas/k8s-statefulset.js +++ b/src/schemas/k8s-statefulset.js @@ -10,242 +10,348 @@ export const k8sStatefulSetSchema = { { title: '基础信息', fields: [ - { - key: 'name', - label: 'StatefulSet 名称', - type: 'text', - placeholder: 'my-statefulset', - default: 'my-statefulset', - required: true, - }, - { - key: 'namespace', - 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, - }, + { key: 'name', label: 'StatefulSet 名称', type: 'text', placeholder: 'my-statefulset', default: 'my-statefulset', required: true }, + { key: 'namespace', 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 }, + { key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [ + { label: 'IfNotPresent (推荐)', value: 'IfNotPresent' }, + { label: 'Always (每次拉取)', value: 'Always' }, + { label: 'Never (仅本地)', value: 'Never' }, + ], default: 'IfNotPresent' }, ], }, { title: '网络与服务', fields: [ - { - key: 'containerPort', - label: '容器端口', - type: 'number', - min: 1, - max: 65535, - default: 80, - }, - { - key: 'serviceName', - label: 'Headless Service 名称', - type: 'text', - placeholder: 'my-service', - default: 'my-service', - required: true, - tip: '必须已存在的 Headless Service', - }, + { 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' }, + { key: 'serviceName', label: 'Headless Service 名称', type: 'text', placeholder: 'my-service', default: 'my-service', required: true, tip: '必须已存在的 Headless Service' }, ], }, { title: '持久存储', fields: [ - { - key: 'volumeClaimEnabled', - label: '启用 PVC', - type: 'switch', - default: true, - tip: '为每个 Pod 创建独立的 PersistentVolumeClaim', - }, - { - 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 }, - }, + { key: 'volumeClaimEnabled', label: '启用 PVC', type: 'switch', default: true, tip: '为每个 Pod 创建独立的 PersistentVolumeClaim' }, + { 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: '资源限制', 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' }, - ], - default: '256Mi', - }, + { 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: 'updateStrategy', - label: '更新策略', - type: 'select', - options: [ - { label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' }, - { label: 'OnDelete - 手动删除触发', value: 'OnDelete' }, - ], - default: 'RollingUpdate', - }, - { - key: 'partition', - label: 'Partition', - type: 'number', - min: 0, - max: 100, - default: 0, - tip: '仅更新序号 >= partition 的 Pod', - dependsOn: { key: 'updateStrategy', value: 'RollingUpdate' }, - }, + { key: 'updateStrategy', label: '更新策略', type: 'select', options: [ + { label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' }, + { label: 'OnDelete - 手动删除触发', value: 'OnDelete' }, + ], default: 'RollingUpdate' }, + { key: 'partition', label: 'Partition', type: 'number', min: 0, max: 100, default: 0, tip: '仅更新序号 >= partition 的 Pod', dependsOn: { key: 'updateStrategy', value: 'RollingUpdate' } }, + ], + }, + { + title: '启动探针 (Startup Probe)', + 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: 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: '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) { - const lines = [] - const prefix = ' '.repeat(indent) +// ======================== YAML 生成器 ======================== - for (const [key, value] of Object.entries(obj)) { - if (value === null || value === undefined || value === '') continue +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 (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]}: ${first[1]}`) - for (let i = 1; i < entries.length; i++) { - lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`) - } - } else { - lines.push(`${prefix} - ${item}`) - } - } - } else if (typeof value === 'object') { - lines.push(`${prefix}${key}:`) - lines.push(toYaml(value, indent + 1)) + 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 { - 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) { - const labels = { app: config.appName } + const labels = { app: config.appLabel || config.appName } + if (config.versionLabel) labels.version = config.versionLabel const container = { name: config.appName, image: config.image, - ports: [{ containerPort: config.containerPort, name: 'http' }], + 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, + } + } + + // Volume mounts for PVC const volumeMounts = [] const volumeClaimTemplates = [] @@ -265,25 +371,48 @@ export function generateK8sStatefulSetYaml(config) { 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 = { apiVersion: 'apps/v1', kind: 'StatefulSet', - metadata: { - name: config.name, - namespace: config.namespace, - labels, - }, + metadata: { name: config.name, namespace: config.namespace, labels }, spec: { replicas: config.replicas, serviceName: config.serviceName, - selector: { matchLabels: { app: config.appName } }, + selector: { matchLabels: { app: config.appLabel || config.appName } }, updateStrategy: config.updateStrategy === 'RollingUpdate' ? { type: 'RollingUpdate', rollingUpdate: { partition: config.partition } } : { type: 'OnDelete' }, - template: { - metadata: { labels }, - spec: { containers: [container] }, - }, + template: { metadata: { labels }, spec: podSpec }, }, } @@ -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` - 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') +} diff --git a/src/schemas/nginx.js b/src/schemas/nginx.js index 3a0edb1..9f8c41e 100644 --- a/src/schemas/nginx.js +++ b/src/schemas/nginx.js @@ -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(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`) - lines.push(` # upstream backend {`) - lines.push(` # server 127.0.0.1:8080;`) - lines.push(` # }`) - lines.push(``) + // 限流配置 + if (config.enableLimitReq) { + lines.push(` # 限流配置`) + lines.push(` limit_req_zone ${config.limitReqZone};`) + 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(` listen ${config.httpPort};`) @@ -308,10 +441,19 @@ export function generateNginxConf(config) { lines.push(``) } + // 限流应用到 location lines.push(` location / {`) + if (config.enableLimitReq) { + lines.push(` limit_req zone=one burst=${config.limitReqBurst} nodelay;`) + } + 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_set_header Upgrade $http_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-Proto $scheme;`) 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 { lines.push(` root /usr/share/nginx/html;`) lines.push(` index index.html index.htm;`) diff --git a/src/schemas/postgresql.js b/src/schemas/postgresql.js index ab028db..278a5a6 100644 --- a/src/schemas/postgresql.js +++ b/src/schemas/postgresql.js @@ -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(`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') } diff --git a/src/schemas/redis.js b/src/schemas/redis.js index d3a3773..6cf18dc 100644 --- a/src/schemas/redis.js +++ b/src/schemas/redis.js @@ -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(`maxclients ${config.maxclients}`) 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') }