8 Commits

Author SHA1 Message Date
Your Name 70e196045c fix: 修复 Ubuntu Netplan static/dhcp 模式缺少 ethernets 层导致 netplan apply 报错
接口名直接挂在 network: 下导致 'unknown key' 错误,
现在正确放在 ethernets: 下面。
2026-08-07 10:35:47 +08:00
Your Name c9ed2488e6 build: 设置 base: './' 支持静态文件直接打开 2026-08-05 17:21:43 +08:00
Your Name e5ef6792d1 feat: 新增网卡配置文件生成器(ifcfg/NM keyfile/Netplan)
- 新增 3 个网络配置生成器,覆盖 CentOS/RHEL 7-9 + Ubuntu 18.04-24.04:
  - network-rhel-ifcfg: RHEL/CentOS 7-8 ifcfg 风格(network-scripts)
  - network-rhel-nmkeyfile: RHEL/CentOS 9 NetworkManager keyfile
  - network-ubuntu-netplan: Ubuntu Netplan YAML
- 每个生成器支持 5 种配置类型: 静态IP / DHCP / Bond / Bridge / VLAN
- Bond 支持 7 种模式(0-6),含 miimon/lacp_rate/成员网卡配置
- 支持路由 Metric 配置(ifcfg METRIC= / NM route-metric= / Netplan metric:)
- ConfigForm 增加 valueIn 依赖支持(修复 keepalived 已有但失效的 valueIn bug)
- 新增「网络配置」侧边栏分类,组件计数 60 -> 63
2026-08-05 17:16:41 +08:00
Your Name 02d514fe7d fix(schemas): 修复 nginx/apache/bind/syslog 多个生成配置 bug
通过本地真实工具(nginx -t / named-checkconf / apache2 -t / redis-server)
对全部 60 个 schema 进行 dry-run 验证,发现并修复:

nginx (src/schemas/nginx.js):
  - 'listen ... http2' 是 nginx 1.25+ 废弃写法,改为 listen 443 ssl + http2 on
  - 修复 'add_header not allowed here' 错误:原模板把 CORS 头写到了关闭的
    server 块外(落到 http 顶层),CORS 头重写到 server 作用域内
  - 'if' 块内 add_header 在 nginx 1.22+ 直接 emerg;改为只在 if 里 'return 204',
    CORS 头由 server 作用域 add_header always 继承到 204 响应
  - upstreamServers 默认值漏 ';' (server 127.0.0.1:8080 缺分号),
    模板自动补全 + 默认值修正
  - add_header X-Cache-Status 加 always,避免清空 CORS 头

apache (src/schemas/apache.js):
  - mod_log_config 是 built-in 核心模块,移除 LoadModule 行(会报 'is built-in')
  - maxRequestWorkers 默认 256 与 threadsPerChild=25 不整除,改为 250

bind (src/schemas/bind.js):
  - ACL 列表 { 127.0.0.1; any } 缺末尾 ';',BIND 9.11+ 报 'missing ; before }'
    加 acl() 工具自动补 ';',对 listen-on/allow-query/allow-recursion 均生效
  - dnssec-enable 在 BIND 9.16+ 已删除(自动启用),改为注释
  - 删掉硬编码的 'include /etc/named.rfc1912.zones' 与 named.root.key
    (不同发行版路径不同,9.16+ 默认不存在,会导致启动失败)

syslog (src/schemas/syslog.js):
  - 启用 enableHighPrecisionTimestamps 时会重复输出 'ActionFileDefaultTemplate'
    (rsyslog 警告)。改为仅在 logFormat 不是 RSYSLOG_FileFormat 时输出自定义
    template(RSYSLOG_FileFormat 自带微秒精度)

本机验证: nginx / bind 9.18 / apache 2.4 / redis 7.0 全部 PASS;
26 个 YAML / 3 个 XML / 1 个 JSON schema 全部可解析。
2026-07-28 16:46:09 +08:00
cnbugs 0ae7a5858f fix: 注册 Dockerfile 到 Schema 中心 2026-07-24 16:26:50 +08:00
cnbugs 6426eedab5 feat: 新增 Dockerfile 配置模板,总计 60 个组件
Dockerfile 支持:
- 基础镜像 (FROM) + 多阶段构建 + 平台架构
- 元数据 LABEL / 构建参数 ARG
- 环境变量 ENV / 工作目录 WORKDIR
- 文件复制 COPY + chown/chmod + ADD (自动解压/URL)
- 依赖安装: apk/apt/yum/dnf 包管理器 + 应用依赖 + 构建命令
- 端口 EXPOSE / 数据卷 VOLUME
- 用户 USER (非 root 运行)
- 健康检查 HEALTHCHECK (interval/timeout/retries/start-period)
- 入口 ENTRYPOINT / CMD (exec/shell 格式)
- 停止信号 STOPSIGNAL / Shell 指令
2026-07-24 15:12:46 +08:00
cnbugs 4e98eb8c4a feat: 8 个核心 schema 生产级增强
K8S 工作负载 (5个):
- StatefulSet: +探针(Startup/Liveness/Readiness) +环境变量 +节点亲和性 +Pod反亲和性 +容忍度 +安全上下文 +ServiceAccount
- DaemonSet: +容器端口 +探针 +环境变量 +卷挂载 +节点亲和性 +安全上下文 +ServiceAccount
- Pod: +探针 +卷挂载(5种类型) +NodeSelector +节点亲和性 +Pod反亲和性 +容忍度 +安全上下文 +HostNetwork +DNSPolicy
- CronJob: +容器端口 +资源限制 +环境变量 +卷挂载 +安全上下文 +ServiceAccount
- Job: +容器端口 +资源限制 +环境变量 +卷挂载 +安全上下文 +ServiceAccount

中间件 (3个):
- Redis: +ACL配置 +慢日志 +延迟监控 +客户端管理(ioThreads/lazyfree)
- PostgreSQL: +连接池 +流复制(GTID/WAL) +Autovacuum调优
- NGINX: +限流(limit_req) +Upstream负载均衡(least_conn/ip_hash) +缓存(proxy_cache)
2026-07-22 15:00:34 +08:00
cnbugs e258969c37 feat: K8s Deployment 全面增强,补充生产级配置
新增 11 个配置分组:
- 启动探针 (Startup Probe): 类型、路径/端口/命令、初始延迟、周期、失败阈值、超时
- 存活探针 (Liveness Probe): 完整参数 + HTTP Headers + Exec 命令
- 就绪探针 (Readiness Probe): 完整参数 + HTTP Headers + Exec 命令
- 环境变量: KEY=VALUE 直接输入、从 ConfigMap/Secret 导入
- 卷挂载: emptyDir / PVC / ConfigMap / Secret / HostPath
- NodeSelector: 简单节点标签匹配
- 节点亲和性 (Node Affinity): required / preferred + In/NotIn/Exists/Gt/Lt
- Pod 反亲和性: 跨节点/可用区分散副本
- 容忍度 (Tolerations): NoSchedule / PreferNoSchedule / NoExecute
- 安全上下文: runAsUser、readOnlyRootFilesystem、fsGroup
- 部署策略增强: revisionHistoryLimit、progressDeadlineSeconds、minReadySeconds
- 标签增强: ServiceAccount、优雅终止时间
2026-07-22 14:42:14 +08:00
21 changed files with 4109 additions and 1112 deletions
-39
View File
@@ -644,9 +644,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -660,9 +657,6 @@
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -676,9 +670,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -692,9 +683,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -708,9 +696,6 @@
"cpu": [
"loong64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -724,9 +709,6 @@
"cpu": [
"loong64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -740,9 +722,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -756,9 +735,6 @@
"cpu": [
"ppc64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -772,9 +748,6 @@
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -788,9 +761,6 @@
"cpu": [
"riscv64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -804,9 +774,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -820,9 +787,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -836,9 +800,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
+1
View File
@@ -141,6 +141,7 @@ function isFieldVisible(field) {
const dep = field.dependsOn
const val = formData.value[dep.key]
if ('value' in dep) return val === dep.value
if ('valueIn' in dep) return dep.valueIn.includes(val)
if ('valueNotIn' in dep) return !dep.valueNotIn.includes(val)
return true
}
+1 -1
View File
@@ -60,7 +60,7 @@
</div>
<div class="sidebar-footer">
<el-text size="small" type="info">v1.0.0 · 纯前端生成 · 59 个组件</el-text>
<el-text size="small" type="info">v1.0.0 · 纯前端生成 · 63 个组件</el-text>
</div>
</div>
</template>
+3 -2
View File
@@ -94,7 +94,8 @@ export const apacheSchema = {
type: 'number',
min: 1,
max: 10000,
default: 256,
default: 250,
tip: 'worker/event 模式下应为 threadsPerChild 的整数倍(默认 25×10=250',
},
{
key: 'threadsPerChild',
@@ -356,7 +357,7 @@ export function generateApacheConf(config) {
if (config.enableModSecurity) lines.push(`LoadModule security2_module modules/mod_security2.so`)
lines.push(`LoadModule dir_module modules/mod_dir.so`)
lines.push(`LoadModule mime_module modules/mod_mime.so`)
lines.push(`LoadModule log_config_module modules/mod_log_config.so`)
// mod_log_config 是 built-in(核心模块),无需 LoadModule
lines.push(`LoadModule headers_module modules/mod_headers.so`)
lines.push(``)
+19 -7
View File
@@ -163,14 +163,23 @@ export function generateBindConf(config) {
// Options block
lines.push(`// ======================== 全局选项 ========================`)
lines.push(`options {`)
lines.push(` listen-on port 53 { ${config.listenOn} };`)
lines.push(` listen-on-v6 port 53 { ${config.listenOnV6} };`)
// ACL 列表辅助:把 "127.0.0.1; any" 变成 { 127.0.0.1; any; }
// 每个元素都自动补分号(解决 BIND 9.11+ 报 "missing ';' before '}'" 的坑)
const acl = (raw) => {
if (!raw) return ''
const parts = raw.split(';').map((s) => s.trim()).filter(Boolean)
return parts.map((p) => `${p};`).join(' ')
}
lines.push(` listen-on port 53 { ${acl(config.listenOn)} };`)
lines.push(` listen-on-v6 port 53 { ${acl(config.listenOnV6)} };`)
lines.push(` directory "/var/named";`)
lines.push(` dump-file "/var/named/data/cache_dump.db";`)
lines.push(` statistics-file "/var/named/data/named_stats.txt";`)
lines.push(` memstatistics-file "/var/named/data/named_mem_stats.txt";`)
lines.push(` allow-query { ${config.allowQuery} };`)
lines.push(` allow-recursion { ${config.allowRecursion} };`)
lines.push(` allow-query { ${acl(config.allowQuery)} };`)
lines.push(` allow-recursion { ${acl(config.allowRecursion)} };`)
lines.push(` recursion ${config.recursion ? 'yes' : 'no'};`)
lines.push(``)
lines.push(` forwarders {`)
@@ -182,8 +191,9 @@ export function generateBindConf(config) {
lines.push(` forward ${config.forwardType};`)
lines.push(``)
// BIND 9.16+ 已移除 dnssec-enableDNSSEC 自动开启),无需再写
if (config.enableDnssec) {
lines.push(` dnssec-enable yes;`)
lines.push(` // dnssec-enable yes; // BIND 9.16+ 已自动启用,无需此指令`)
}
if (config.dnssecValidation) {
lines.push(` dnssec-validation yes;`)
@@ -245,8 +255,10 @@ export function generateBindConf(config) {
lines.push(``)
}
lines.push(`include "/etc/named.rfc1912.zones";`)
lines.push(`include "/etc/named.root.key";`)
// 这些 include 在 BIND 9.16+ 不再默认存在,硬编码会导致启动失败
// (不同发行版路径不同:Debian /etc/bind/named.conf.default-zonesRHEL /etc/named.rfc1912.zones
// 如需本地 RFC1918 反向区域,请用户根据实际系统手动添加 include
// lines.push(`include "/etc/bind/named.conf.default-zones";`)
return lines.join('\n')
}
+364
View File
@@ -0,0 +1,364 @@
export const dockerfileSchema = {
id: 'dockerfile',
name: 'Dockerfile',
icon: 'Box',
category: '容器化',
description: 'Docker 容器镜像构建文件',
format: 'dockerfile',
fileName: 'Dockerfile',
groups: [
{
title: '基础镜像',
fields: [
{ key: 'baseImage', label: '基础镜像', type: 'text', placeholder: 'nginx:1.24-alpine', default: 'nginx:1.24-alpine', required: true, tip: 'FROM 指令' },
{ key: 'fromPlatform', label: '平台架构', type: 'select', options: [
{ label: '不限制', value: '' },
{ label: 'linux/amd64', value: 'linux/amd64' },
{ label: 'linux/arm64', value: 'linux/arm64' },
], default: '', tip: '多架构构建时指定' },
{ key: 'enableMultiStage', label: '多阶段构建', type: 'switch', default: false, tip: '分离构建和运行阶段' },
{ key: 'builderImage', label: '构建阶段镜像', type: 'text', placeholder: 'node:18-alpine', default: 'node:18-alpine', dependsOn: { key: 'enableMultiStage', value: true } },
{ key: 'builderAs', label: '构建阶段别名', type: 'text', default: 'builder', dependsOn: { key: 'enableMultiStage', value: true } },
],
},
{
title: '元数据 (LABEL)',
fields: [
{ key: 'enableLabels', label: '添加标签', type: 'switch', default: false },
{ key: 'labelMaintainer', label: '维护者', type: 'text', placeholder: 'team@example.com', default: 'team@example.com', dependsOn: { key: 'enableLabels', value: true } },
{ key: 'labelVersion', label: '版本', type: 'text', placeholder: '1.0.0', default: '1.0.0', dependsOn: { key: 'enableLabels', value: true } },
{ key: 'labelDescription', label: '描述', type: 'text', placeholder: 'My application', default: '', dependsOn: { key: 'enableLabels', value: true } },
],
},
{
title: '构建参数 (ARG)',
fields: [
{ key: 'enableArgs', label: '定义构建参数', type: 'switch', default: false, tip: '仅在 docker build --build-arg 时生效' },
{ key: 'args', label: '参数列表 (KEY=VALUE)', type: 'text', placeholder: 'APP_VERSION=1.0.0\nNODE_ENV=production', default: 'APP_VERSION=1.0.0', dependsOn: { key: 'enableArgs', value: true }, tip: '每行一个,格式 KEY 或 KEY=默认值' },
],
},
{
title: '环境变量 (ENV)',
fields: [
{ key: 'enableEnv', label: '设置环境变量', type: 'switch', default: false },
{ key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'NODE_ENV=production\nAPP_PORT=8080', default: 'NODE_ENV=production\nAPP_PORT=8080', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' },
],
},
{
title: '工作目录与用户',
fields: [
{ key: 'enableWorkdir', label: '设置工作目录', type: 'switch', default: true },
{ key: 'workdir', label: '工作目录', type: 'text', default: '/app', dependsOn: { key: 'enableWorkdir', value: true } },
{ key: 'enableUser', label: '切换运行用户', type: 'switch', default: false, tip: '生产环境建议使用非 root 用户' },
{ key: 'userName', label: '用户名或 UID', type: 'text', placeholder: '1001:1001', default: '1001:1001', dependsOn: { key: 'enableUser', value: true } },
{ key: 'enableUseradd', label: '创建用户', type: 'switch', default: false, dependsOn: { key: 'enableUser', value: true }, tip: '在 RUN 中创建用户' },
{ key: 'createUserName', label: '用户名', type: 'text', default: 'appuser', dependsOn: { key: 'enableUseradd', value: true } },
],
},
{
title: '文件复制',
fields: [
{ key: 'enableCopy', label: '复制文件', type: 'switch', default: true },
{ key: 'copySrc', label: '源路径', type: 'text', placeholder: '. 或 ./dist', default: '.', dependsOn: { key: 'enableCopy', value: true } },
{ key: 'copyDest', label: '目标路径', type: 'text', placeholder: '/app', default: '/app', dependsOn: { key: 'enableCopy', value: true } },
{ key: 'enableCopyChown', label: '设置文件所有者', type: 'switch', default: false, dependsOn: { key: 'enableCopy', value: true } },
{ key: 'copyChown', label: '所有者', type: 'text', placeholder: '1001:1001', default: '1001:1001', dependsOn: { key: 'enableCopyChown', value: true } },
{ key: 'enableCopyChmod', label: '设置文件权限', type: 'switch', default: false, dependsOn: { key: 'enableCopy', value: true } },
{ key: 'copyChmod', label: '权限', type: 'text', placeholder: '755', default: '755', dependsOn: { key: 'enableCopyChmod', value: true } },
{ key: 'enableAdd', label: '使用 ADD (自动解压/URL)', type: 'switch', default: false, tip: '需要解压 tar 或下载远程文件时使用' },
{ key: 'addSrc', label: 'ADD 源', type: 'text', placeholder: 'https://example.com/app.tar.gz', default: '', dependsOn: { key: 'enableAdd', value: true } },
{ key: 'addDest', label: 'ADD 目标', type: 'text', default: '/app/', dependsOn: { key: 'enableAdd', value: true } },
],
},
{
title: '依赖安装 (RUN)',
fields: [
{ key: 'packageManager', label: '包管理器', type: 'select', options: [
{ label: '不安装系统包', value: 'none' },
{ label: 'apk (Alpine)', value: 'apk' },
{ label: 'apt (Debian/Ubuntu)', value: 'apt' },
{ label: 'yum (CentOS/RHEL)', value: 'yum' },
{ label: 'dnf (Fedora)', value: 'dnf' },
], default: 'none' },
{ key: 'systemPackages', label: '系统包 (空格分隔)', type: 'text', placeholder: 'curl wget tzdata', default: 'curl ca-certificates tzdata', dependsOn: { key: 'packageManager', valueNotIn: ['none'] } },
{ key: 'enableAppInstall', label: '安装应用依赖', type: 'switch', default: false, tip: 'npm install / pip install / go build 等' },
{ key: 'appInstallCmd', label: '安装命令', type: 'text', placeholder: 'npm ci --only=production', default: 'npm ci --only=production', dependsOn: { key: 'enableAppInstall', value: true } },
{ key: 'enableBuildCmd', label: '构建命令', type: 'switch', default: false, tip: 'npm run build / go build / mvn package 等' },
{ key: 'buildCmd', label: '构建命令', type: 'text', placeholder: 'npm run build', default: 'npm run build', dependsOn: { key: 'enableBuildCmd', value: true } },
{ key: 'enableCleanup', label: '清理缓存', type: 'switch', default: true, dependsOn: { key: 'packageManager', valueNotIn: ['none'] }, tip: '减小镜像体积' },
],
},
{
title: '端口与网络',
fields: [
{ key: 'enableExpose', label: '暴露端口', type: 'switch', default: true },
{ key: 'exposePorts', label: '端口列表', type: 'text', placeholder: '80 443', default: '80', dependsOn: { key: 'enableExpose', value: true }, tip: '空格分隔多个端口' },
],
},
{
title: '健康检查 (HEALTHCHECK)',
fields: [
{ key: 'enableHealthcheck', label: '添加健康检查', type: 'switch', default: false },
{ key: 'healthcheckType', label: '检查方式', type: 'select', options: [
{ label: 'CMD (自定义命令)', value: 'CMD' },
{ label: 'NONE (禁用继承的健康检查)', value: 'NONE' },
], default: 'CMD', dependsOn: { key: 'enableHealthcheck', value: true } },
{ key: 'healthcheckInterval', label: '检查间隔', type: 'text', default: '30s', dependsOn: { key: 'healthcheckType', value: 'CMD' } },
{ key: 'healthcheckTimeout', label: '超时时间', type: 'text', default: '10s', dependsOn: { key: 'healthcheckType', value: 'CMD' } },
{ key: 'healthcheckRetries', label: '重试次数', type: 'number', min: 1, max: 10, default: 3, dependsOn: { key: 'healthcheckType', value: 'CMD' } },
{ key: 'healthcheckStartPeriod', label: '启动等待期', type: 'text', default: '5s', dependsOn: { key: 'healthcheckType', value: 'CMD' } },
{ key: 'healthcheckCmd', label: '检查命令', type: 'text', placeholder: 'curl -f http://localhost/ || exit 1', default: 'curl -f http://localhost:80/ || exit 1', dependsOn: { key: 'healthcheckType', value: 'CMD' }, required: true },
],
},
{
title: '挂载点与数据卷',
fields: [
{ key: 'enableVolume', label: '声明数据卷', type: 'switch', default: false },
{ key: 'volumePaths', label: '挂载路径', type: 'text', placeholder: '/data /var/log/app', default: '/data', dependsOn: { key: 'enableVolume', value: true }, tip: '空格分隔多个路径' },
],
},
{
title: '入口与启动',
fields: [
{ key: 'enableEntrypoint', label: '设置 ENTRYPOINT', type: 'switch', default: false, tip: '容器启动时执行的固定命令' },
{ key: 'entrypointCmd', label: 'ENTRYPOINT 命令', type: 'text', placeholder: '["java", "-jar", "app.jar"]', default: '["node", "server.js"]', dependsOn: { key: 'enableEntrypoint', value: true }, tip: 'JSON 数组格式或 shell 格式' },
{ key: 'enableCmd', label: '设置 CMD', type: 'switch', default: true, tip: '容器启动时的默认参数' },
{ key: 'cmdType', label: 'CMD 格式', type: 'select', options: [
{ label: 'exec 格式 (推荐)', value: 'exec' },
{ label: 'shell 格式', value: 'shell' },
], default: 'exec', dependsOn: { key: 'enableCmd', value: true } },
{ key: 'cmdValue', label: 'CMD 内容', type: 'text', placeholder: '["nginx", "-g", "daemon off;"]', default: '["nginx", "-g", "daemon off;"]', dependsOn: { key: 'enableCmd', value: true } },
],
},
{
title: '信号与停止',
fields: [
{ key: 'enableStopSignal', label: '自定义停止信号', type: 'switch', default: false, tip: 'docker stop 发送的信号' },
{ key: 'stopSignal', label: '停止信号', type: 'select', options: [
{ label: 'SIGTERM (默认)', value: 'SIGTERM' },
{ label: 'SIGQUIT', value: 'SIGQUIT' },
{ label: 'SIGINT', value: 'SIGINT' },
{ label: 'SIGUSR1', value: 'SIGUSR1' },
], default: 'SIGTERM', dependsOn: { key: 'enableStopSignal', value: true } },
{ key: 'enableShell', label: '指定 Shell', type: 'switch', default: false, tip: 'SHELL 指令,RUN 使用的 shell' },
{ key: 'shellPath', label: 'Shell 路径', type: 'text', default: '/bin/sh', dependsOn: { key: 'enableShell', value: true } },
{ key: 'shellFlag', label: 'Shell 参数', type: 'text', default: '-c', dependsOn: { key: 'enableShell', value: true } },
],
},
],
}
export function generateDockerfile(config) {
const lines = []
lines.push(`# Dockerfile - 由 ConfTemplate 生成`)
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
lines.push(``)
// Builder stage
if (config.enableMultiStage) {
lines.push(`# ======================== 构建阶段 ========================`)
lines.push(`FROM${config.fromPlatform ? ` --platform=${config.fromPlatform}` : ''} ${config.builderImage} AS ${config.builderAs}`)
lines.push(``)
if (config.enableWorkdir) {
lines.push(`WORKDIR ${config.workdir}`)
}
lines.push(`COPY . .`)
if (config.enableAppInstall && config.appInstallCmd) {
lines.push(`RUN ${config.appInstallCmd}`)
}
if (config.enableBuildCmd && config.buildCmd) {
lines.push(`RUN ${config.buildCmd}`)
}
lines.push(``)
lines.push(`# ======================== 运行阶段 ========================`)
}
// FROM
const platformArg = config.fromPlatform ? ` --platform=${config.fromPlatform}` : ''
if (config.enableMultiStage) {
lines.push(`FROM${platformArg} ${config.baseImage}`)
} else {
lines.push(`FROM${platformArg} ${config.baseImage}`)
}
lines.push(``)
// ARG
if (config.enableArgs && config.args) {
config.args.split('\n').filter(Boolean).forEach(line => {
lines.push(`ARG ${line.trim()}`)
})
lines.push(``)
}
// LABEL
if (config.enableLabels) {
if (config.labelMaintainer) lines.push(`LABEL maintainer="${config.labelMaintainer}"`)
if (config.labelVersion) lines.push(`LABEL version="${config.labelVersion}"`)
if (config.labelDescription) lines.push(`LABEL description="${config.labelDescription}"`)
lines.push(``)
}
// ENV
if (config.enableEnv && config.envVars) {
config.envVars.split('\n').filter(Boolean).forEach(line => {
lines.push(`ENV ${line.trim()}`)
})
lines.push(``)
}
// WORKDIR
if (config.enableWorkdir) {
lines.push(`WORKDIR ${config.workdir}`)
lines.push(``)
}
// Package install
if (config.packageManager && config.packageManager !== 'none') {
const pkgs = (config.systemPackages || '').trim()
if (pkgs) {
if (config.packageManager === 'apk') {
lines.push(`RUN apk add --no-cache ${pkgs}`)
} else if (config.packageManager === 'apt') {
lines.push(`RUN apt-get update && \\`)
lines.push(` apt-get install -y --no-install-recommends ${pkgs} && \\`)
if (config.enableCleanup) {
lines.push(` rm -rf /var/lib/apt/lists/*`)
}
} else if (config.packageManager === 'yum') {
lines.push(`RUN yum install -y ${pkgs} && \\`)
if (config.enableCleanup) {
lines.push(` yum clean all && rm -rf /var/cache/yum`)
}
} else if (config.packageManager === 'dnf') {
lines.push(`RUN dnf install -y ${pkgs} && \\`)
if (config.enableCleanup) {
lines.push(` dnf clean all && rm -rf /var/cache/dnf`)
}
}
lines.push(``)
}
}
// Create user
if (config.enableUser && config.enableUseradd && config.createUserName) {
lines.push(`RUN addgroup -S ${config.createUserName} && adduser -S ${config.createUserName} -G ${config.createUserName}`)
lines.push(``)
}
// COPY from builder stage
if (config.enableMultiStage) {
if (config.enableCopyChown) {
lines.push(`COPY --from=${config.builderAs} --chown=${config.copyChown} ${config.workdir || '/app'} ${config.copyDest}`)
} else {
lines.push(`COPY --from=${config.builderAs} ${config.workdir || '/app'} ${config.copyDest}`)
}
lines.push(``)
}
// COPY
if (config.enableCopy && !config.enableMultiStage) {
let copyFlags = []
if (config.enableCopyChown && config.copyChown) copyFlags.push(`--chown=${config.copyChown}`)
if (config.enableCopyChmod && config.copyChmod) copyFlags.push(`--chmod=${config.copyChmod}`)
const flags = copyFlags.length ? ` ${copyFlags.join(' ')}` : ''
lines.push(`COPY${flags} ${config.copySrc} ${config.copyDest}`)
lines.push(``)
}
// ADD
if (config.enableAdd && config.addSrc) {
lines.push(`ADD ${config.addSrc} ${config.addDest}`)
lines.push(``)
}
// App install (non-multi-stage)
if (!config.enableMultiStage && config.enableAppInstall && config.appInstallCmd) {
lines.push(`RUN ${config.appInstallCmd}`)
lines.push(``)
}
// Build cmd (non-multi-stage)
if (!config.enableMultiStage && config.enableBuildCmd && config.buildCmd) {
lines.push(`RUN ${config.buildCmd}`)
lines.push(``)
}
// VOLUME
if (config.enableVolume && config.volumePaths) {
const paths = config.volumePaths.split(/\s+/).filter(Boolean)
if (paths.length === 1) {
lines.push(`VOLUME ["${paths[0]}"]`)
} else {
lines.push(`VOLUME [${paths.map(p => `"${p}"`).join(', ')}]`)
}
lines.push(``)
}
// EXPOSE
if (config.enableExpose && config.exposePorts) {
lines.push(`EXPOSE ${config.exposePorts}`)
lines.push(``)
}
// USER
if (config.enableUser) {
lines.push(`USER ${config.userName}`)
lines.push(``)
}
// HEALTHCHECK
if (config.enableHealthcheck) {
if (config.healthcheckType === 'NONE') {
lines.push(`HEALTHCHECK NONE`)
} else {
let hc = `HEALTHCHECK`
if (config.healthcheckInterval) hc += ` --interval=${config.healthcheckInterval}`
if (config.healthcheckTimeout) hc += ` --timeout=${config.healthcheckTimeout}`
if (config.healthcheckRetries) hc += ` --retries=${config.healthcheckRetries}`
if (config.healthcheckStartPeriod) hc += ` --start-period=${config.healthcheckStartPeriod}`
hc += ` CMD ${config.healthcheckCmd}`
lines.push(hc)
}
lines.push(``)
}
// STOPSIGNAL
if (config.enableStopSignal) {
lines.push(`STOPSIGNAL ${config.stopSignal}`)
lines.push(``)
}
// SHELL
if (config.enableShell) {
lines.push(`SHELL ["${config.shellPath}", "${config.shellFlag}"]`)
lines.push(``)
}
// ENTRYPOINT
if (config.enableEntrypoint && config.entrypointCmd) {
const val = config.entrypointCmd.trim()
if (val.startsWith('[')) {
lines.push(`ENTRYPOINT ${val}`)
} else {
lines.push(`ENTRYPOINT ["${val}"]`)
}
lines.push(``)
}
// CMD
if (config.enableCmd && config.cmdValue) {
const val = config.cmdValue.trim()
if (config.cmdType === 'exec') {
if (val.startsWith('[')) {
lines.push(`CMD ${val}`)
} else {
lines.push(`CMD ["${val}"]`)
}
} else {
lines.push(`CMD ${val}`)
}
lines.push(``)
}
return lines.join('\n')
}
+23 -1
View File
@@ -61,6 +61,7 @@ import { etcdSchema, generateEtcdYml } from './etcd'
// ============ 容器化 ============
import { dockerSchema, generateDockerDaemonJson } from './docker'
import { dockerComposeSchema, generateDockerComposeYml } from './docker-compose'
import { dockerfileSchema, generateDockerfile } from './dockerfile'
// ============ 日志监控 ============
import { logstashSchema, generateLogstashConf } from './logstash'
@@ -94,6 +95,11 @@ import { sersyncSchema, generateSersyncXml } from './sersync'
import { vsftpdSchema, generateVsftpdConf } from './vsftpd'
import { subversionSchema, generateSubversionConf } from './subversion'
// ============ 网络配置 ============
import { networkRhelIfcfgSchema, generateNetworkRhelIfcfg } from './network-rhel-ifcfg'
import { networkRhelNmKeyfileSchema, generateNetworkRhelNmKeyfile } from './network-rhel-nmkeyfile'
import { networkUbuntuNetplanSchema, generateNetworkUbuntuNetplan } from './network-ubuntu-netplan'
// ============ 数据库中间件 ============
import { mycatSchema, generateMycatServerXml } from './mycat'
@@ -152,6 +158,7 @@ export const schemas = {
// 容器化
docker: dockerSchema,
'docker-compose': dockerComposeSchema,
dockerfile: dockerfileSchema,
// 日志监控
logstash: logstashSchema,
kibana: kibanaSchema,
@@ -181,6 +188,10 @@ export const schemas = {
subversion: subversionSchema,
// 数据库中间件
mycat: mycatSchema,
// 网络配置
'network-rhel-ifcfg': networkRhelIfcfgSchema,
'network-rhel-nmkeyfile': networkRhelNmKeyfileSchema,
'network-ubuntu-netplan': networkUbuntuNetplanSchema,
}
// ============================================================
@@ -224,6 +235,7 @@ export const generators = {
etcd: generateEtcdYml,
docker: generateDockerDaemonJson,
'docker-compose': generateDockerComposeYml,
dockerfile: generateDockerfile,
logstash: generateLogstashConf,
kibana: generateKibanaYml,
prometheus: generatePrometheusYml,
@@ -251,6 +263,10 @@ export const generators = {
subversion: generateSubversionConf,
// 数据库中间件
mycat: generateMycatServerXml,
// 网络配置
'network-rhel-ifcfg': generateNetworkRhelIfcfg,
'network-rhel-nmkeyfile': generateNetworkRhelNmKeyfile,
'network-ubuntu-netplan': generateNetworkUbuntuNetplan,
}
// ============================================================
@@ -326,7 +342,7 @@ export const categories = [
{
name: '容器化',
icon: 'Box',
items: ['docker', 'docker-compose'],
items: ['docker', 'docker-compose', 'dockerfile'],
},
{
name: '日志监控',
@@ -348,6 +364,11 @@ export const categories = [
icon: 'Connection',
items: ['keepalived', 'haproxy', 'lvs'],
},
{
name: '网络配置',
icon: 'Connection',
items: ['network-rhel-ifcfg', 'network-rhel-nmkeyfile', 'network-ubuntu-netplan'],
},
{
name: '系统服务',
icon: 'Setting',
@@ -388,6 +409,7 @@ export function getLanguage(format) {
properties: 'properties',
ini: 'ini',
xml: 'xml',
dockerfile: 'dockerfile',
}
return map[format] || 'plaintext'
}
+252 -25
View File
@@ -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,52 +134,264 @@ 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: '完成后自动删除 Job0 表示保留',
},
],
},
{
title: '容器端口',
fields: [
{ key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
{ key: 'portName', label: '端口名称', type: 'text', default: 'http' },
{ key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
],
},
{
title: '资源限制',
fields: [
{ key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
{ label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
{ label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
], default: '100m' },
{ key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
{ label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
{ label: '2', value: '2' },{ label: '4', value: '4' },
], default: '500m' },
{ key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
{ label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
{ label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
], default: '128Mi' },
{ key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
{ label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },{ label: '4Gi', value: '4Gi' },
], default: '256Mi' },
],
},
{
title: '环境变量',
fields: [
{ key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false },
{ key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'APP_ENV=production\nLOG_LEVEL=info', default: 'APP_ENV=production\nLOG_LEVEL=info', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' },
{ key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false },
{ key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } },
{ key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
{ key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
],
},
{
title: '卷挂载',
fields: [
{ key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false },
{ key: 'volumeType', label: '卷类型', type: 'select', options: [
{ label: 'emptyDir - 临时目录', value: 'emptyDir' },
{ label: 'PersistentVolumeClaim', value: 'pvc' },
{ label: 'ConfigMap', value: 'configMap' },
{ label: 'Secret', value: 'secret' },
{ label: 'HostPath - 主机路径', value: 'hostPath' },
], default: 'emptyDir', dependsOn: { key: 'enableVolumeMount', value: true } },
{ key: 'volumeName', label: '卷名称', type: 'text', default: 'data', dependsOn: { key: 'enableVolumeMount', value: true } },
{ key: 'volumeMountPath', label: '挂载路径', type: 'text', default: '/data', dependsOn: { key: 'enableVolumeMount', value: true } },
{ key: 'volumeReadOnly', label: '只读挂载', type: 'switch', default: false, dependsOn: { key: 'enableVolumeMount', value: true } },
{ key: 'pvcClaimName', label: 'PVC 名称', type: 'text', default: 'my-pvc', dependsOn: { key: 'volumeType', value: 'pvc' } },
{ key: 'configMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'volumeType', value: 'configMap' } },
{ key: 'secretVolumeName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'volumeType', value: 'secret' } },
{ key: 'hostPath', label: '主机路径', type: 'text', default: '/tmp/data', dependsOn: { key: 'volumeType', value: 'hostPath' } },
],
},
{
title: '安全上下文 (Security Context)',
fields: [
{ key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false },
{ key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
{ key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
{ key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } },
{ key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' },
{ key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } },
{ key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' },
],
},
{
title: 'ServiceAccount',
fields: [
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
],
},
],
}
function toYaml(obj, indent = 0) {
const lines = []
const prefix = ' '.repeat(indent)
// ======================== YAML 生成器 ========================
for (const [key, value] of Object.entries(obj)) {
function toYaml(obj, indent) {
indent = indent || 0
var lines = []
var prefix = ''
for (var p = 0; p < indent; p++) prefix += ' '
var keys = Object.keys(obj)
for (var ki = 0; ki < keys.length; ki++) {
var key = keys[ki]
var value = obj[key]
if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`)
for (const item of value) {
lines.push(prefix + key + ':')
for (var ai = 0; ai < value.length; ai++) {
var item = value[ai]
if (typeof item === 'object' && item !== null) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue
const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
}
lines.push(renderArrayObject(item, indent + 1))
} else {
lines.push(`${prefix} - ${item}`)
lines.push(prefix + ' - ' + item)
}
}
} else if (typeof value === 'object') {
lines.push(`${prefix}${key}:`)
lines.push(prefix + key + ':')
lines.push(toYaml(value, indent + 1))
} else {
lines.push(`${prefix}${key}: ${value}`)
lines.push(prefix + key + ': ' + value)
}
}
return lines.join('\n')
}
function renderArrayObject(item, indent) {
var prefix = ''
for (var p = 0; p < indent; p++) prefix += ' '
var entries = Object.entries(item).filter(function(pair) { return pair[1] !== null && pair[1] !== undefined && pair[1] !== '' })
if (entries.length === 0) return ''
var lines = []
var first = entries[0]
var fk = first[0], fv = first[1]
if (Array.isArray(fv)) {
lines.push(prefix + '- ' + fk + ':')
for (var ai = 0; ai < fv.length; ai++) {
var arrItem = fv[ai]
if (typeof arrItem === 'object' && arrItem !== null) {
lines.push(renderArrayObject(arrItem, indent + 2))
} else {
lines.push(prefix + ' - ' + arrItem)
}
}
} else if (typeof fv === 'object' && fv !== null) {
lines.push(prefix + '- ' + fk + ':')
lines.push(toYaml(fv, indent + 2))
} else {
lines.push(prefix + '- ' + fk + ': ' + fv)
}
for (var i = 1; i < entries.length; i++) {
var k = entries[i][0], v = entries[i][1]
if (v === null || v === undefined || v === '') continue
if (Array.isArray(v)) {
lines.push(prefix + ' ' + k + ':')
for (var ai = 0; ai < v.length; ai++) {
var arrItem = v[ai]
if (typeof arrItem === 'object' && arrItem !== null) {
lines.push(renderArrayObject(arrItem, indent + 2))
} else {
lines.push(prefix + ' - ' + arrItem)
}
}
} else if (typeof v === 'object') {
lines.push(prefix + ' ' + k + ':')
lines.push(toYaml(v, indent + 2))
} else {
lines.push(prefix + ' ' + k + ': ' + v)
}
}
return lines.join('\n')
}
export function generateK8sCronJobYaml(config) {
const container = {
var container = {
name: config.name,
image: config.image,
imagePullPolicy: config.imagePullPolicy,
command: config.command.split(' '),
}
const cronjob = {
// 容器端口
if (config.containerPort) {
container.ports = [{
name: config.portName || 'http',
containerPort: config.containerPort,
protocol: config.protocol || 'TCP',
}]
}
// 资源限制
container.resources = {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
}
// 环境变量
if (config.enableEnv && config.envVars) {
container.env = config.envVars.split('\n').filter(Boolean).map(function(line) {
var idx = line.indexOf('=')
return { name: line.substring(0, idx).trim(), value: line.substring(idx + 1).trim() }
})
}
if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) {
container.envFrom = []
if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } })
if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } })
}
// 卷挂载
var volumes = []
if (config.enableVolumeMount) {
container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }]
var vol = { name: config.volumeName }
var vt = config.volumeType
if (vt === 'emptyDir') vol.emptyDir = {}
else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName }
else if (vt === 'configMap') vol.configMap = { name: config.configMapName }
else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName }
else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' }
volumes.push(vol)
}
// Security Context
if (config.enableSecurityContext) {
container.securityContext = {
runAsUser: config.runAsUser,
runAsGroup: config.runAsGroup,
runAsNonRoot: config.runAsNonRoot,
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
}
}
var podSpec = {
containers: [container],
restartPolicy: config.restartPolicy,
}
// ServiceAccount
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
// Volumes
if (volumes.length) podSpec.volumes = volumes
// fsGroup
if (config.enableSecurityContext && config.fsGroup) {
podSpec.securityContext = { fsGroup: config.fsGroup }
}
var cronjob = {
apiVersion: 'batch/v1',
kind: 'CronJob',
metadata: {
@@ -179,18 +407,17 @@ 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,
},
},
},
},
}
const header = `# K8s CronJob - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'cronjob.yaml'}\n`
var header = '# K8s CronJob - 由 ConfTemplate 生成\n# 生成时间: ' + new Date().toLocaleString('zh-CN') + '\n# 部署命令: kubectl apply -f ' + (config.fileName || 'cronjob.yaml') + '\n'
return header + '\n' + toYaml(cronjob)
}
+329 -179
View File
@@ -10,161 +10,358 @@ export const k8sDaemonSetSchema = {
{
title: '基础信息',
fields: [
{
key: 'name',
label: 'DaemonSet 名称',
type: 'text',
placeholder: 'my-daemonset',
default: 'my-daemonset',
required: true,
{ 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' },
],
},
{
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,
},
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' },
{ 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' },
], 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',
},
], 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: [
{ key: 'updateStrategy', label: '更新策略', type: 'select', options: [
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
], default: 'RollingUpdate' },
],
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)
}
+395 -338
View File
@@ -2,7 +2,7 @@ export const k8sDeploymentSchema = {
id: 'k8s-deployment',
name: 'K8s Deployment',
icon: 'Box',
category: '云原生',
category: 'K8S 工作负载',
description: 'Kubernetes 无状态应用部署控制器',
format: 'yaml',
fileName: 'deployment.yaml',
@@ -10,287 +10,434 @@ export const k8sDeploymentSchema = {
{
title: '基础信息',
fields: [
{
key: 'appName',
label: '应用名称',
type: 'text',
placeholder: 'my-app',
default: 'my-app',
required: true,
tip: '将用作 Deployment 和 Label 的名称',
},
{
key: 'namespace',
label: '命名空间',
type: 'text',
placeholder: 'default',
default: 'default',
},
{
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,
tip: '建议生产环境至少 2 副本',
},
{ key: 'appName', label: '应用名称', type: 'text', default: 'my-app', required: true, tip: 'Deployment 和 Label 名称' },
{ key: 'namespace', label: '命名空间', type: 'text', default: 'default' },
{ 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: 'protocol',
label: '协议',
type: 'select',
options: [
{ label: 'TCP', value: 'TCP' },
{ label: 'UDP', value: 'UDP' },
],
default: 'TCP',
},
{
key: 'portName',
label: '端口名称',
type: 'text',
placeholder: 'http',
default: 'http',
tip: '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' },
],
},
{
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: '健康检查',
title: '启动探针 (Startup Probe)',
fields: [
{
key: 'enableLivenessProbe',
label: '存活探针 (Liveness)',
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' },
{ 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 } },
],
default: 'httpGet',
dependsOn: { key: 'enableLivenessProbe', value: true },
},
{
key: 'livenessPath',
label: '健康检查路径',
type: 'text',
placeholder: '/healthz',
default: '/healthz',
dependsOn: { key: 'livenessProbeType', value: 'httpGet' },
},
{
key: 'livenessPort',
label: '探针端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
dependsOn: { key: 'enableLivenessProbe', value: true },
},
{
key: 'enableReadinessProbe',
label: '就绪探针 (Readiness)',
type: 'switch',
default: true,
tip: '检测容器是否就绪接收流量',
},
{
key: 'readinessProbeType',
label: '探针方式',
type: 'select',
options: [
{ label: 'HTTP GET', value: 'httpGet' },
{ label: 'TCP Socket', value: 'tcpSocket' },
{ label: 'Exec Command', value: 'exec' },
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' },
],
default: 'httpGet',
dependsOn: { key: 'enableReadinessProbe', value: true },
},
{
key: 'readinessPath',
label: '就绪检查路径',
type: 'text',
placeholder: '/ready',
default: '/ready',
dependsOn: { key: 'readinessProbeType', value: 'httpGet' },
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 } },
],
},
{
key: 'readinessPort',
label: '探针端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
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: '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: 'strategy',
label: '更新策略',
type: 'select',
options: [
{ key: 'strategy', label: '更新策略', type: 'select', options: [
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
{ label: 'Recreate - 先删后建', value: 'Recreate' },
],
default: 'RollingUpdate',
},
{
key: 'maxSurge',
label: '最大超出副本数',
type: 'select',
options: [
{ label: '1', value: '1' },
{ label: '25%', value: '25%' },
{ label: '50%', value: '50%' },
{ label: '100%', value: '100%' },
],
default: '25%',
dependsOn: { key: 'strategy', value: 'RollingUpdate' },
},
{
key: 'maxUnavailable',
label: '最大不可用副本数',
type: 'select',
options: [
{ label: '0 (零停机)', value: '0' },
{ label: '1', value: '1' },
{ label: '25%', value: '25%' },
],
default: '0',
dependsOn: { key: 'strategy', value: 'RollingUpdate' },
},
], default: 'RollingUpdate' },
{ key: 'maxSurge', label: '最大超出副本数', type: 'select', options: [
{ label: '0', value: '0' },{ label: '1', value: '1' },{ label: '25%', value: '25%' },
{ label: '50%', value: '50%' },{ label: '100%', value: '100%' },
], default: '25%', dependsOn: { key: 'strategy', value: 'RollingUpdate' } },
{ key: 'maxUnavailable', label: '最大不可用副本数', type: 'select', options: [
{ label: '0 (零停机)', value: '0' },{ label: '1', value: '1' },{ label: '25%', value: '25%' },
], default: '0', dependsOn: { key: 'strategy', value: 'RollingUpdate' } },
{ key: 'revisionHistoryLimit', label: '历史版本保留数', type: 'number', min: 0, max: 50, default: 10, tip: '用于回滚' },
{ key: 'progressDeadlineSeconds', label: '部署超时 (秒)', type: 'number', min: 60, max: 3600, default: 600, tip: '超时则标记为失败' },
{ key: 'minReadySeconds', label: '最小就绪等待 (秒)', type: 'number', min: 0, max: 300, default: 0, tip: '容器就绪后多久视为可用' },
],
},
{
title: '标签与注解',
fields: [
{
key: 'appLabel',
label: 'app 标签值',
type: 'text',
default: '',
tip: '留空则使用应用名称',
},
{
key: 'versionLabel',
label: 'version 标签',
type: 'text',
placeholder: 'v1',
default: 'v1',
},
{
key: 'imagePullPolicy',
label: '镜像拉取策略',
type: 'select',
options: [
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
{ label: 'Always', value: 'Always' },
{ label: 'Never', value: 'Never' },
],
default: 'IfNotPresent',
},
{ 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 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.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 generateK8sDeploymentYaml(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 } })
}
// 卷挂载
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] }
// 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]
}
// Volumes
if (volumes.length) podSpec.volumes = volumes
// fsGroup
if (config.enableSecurityContext && config.fsGroup) {
podSpec.securityContext = { fsGroup: config.fsGroup }
}
const deployment = {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: { name: config.appName, namespace: config.namespace, labels },
spec: {
replicas: config.replicas,
revisionHistoryLimit: config.revisionHistoryLimit,
progressDeadlineSeconds: config.progressDeadlineSeconds,
minReadySeconds: config.minReadySeconds,
selector: { matchLabels: { app: labels.app } },
strategy: config.strategy === 'RollingUpdate'
? { type: 'RollingUpdate', rollingUpdate: { maxSurge: config.maxSurge, maxUnavailable: config.maxUnavailable } }
: { type: 'Recreate' },
template: { metadata: { labels }, spec: podSpec },
},
}
const header = `# K8s Deployment - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'deployment.yaml'}\n`
return header + '\n' + toYaml(deployment)
}
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) {
@@ -298,9 +445,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}`)
@@ -313,104 +469,5 @@ function toYaml(obj, indent = 0) {
lines.push(`${prefix}${key}: ${value}`)
}
}
return lines.join('\n')
}
export function generateK8sDeploymentYaml(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,
},
},
}
const probeConfig = (type, path, port) => {
if (type === 'httpGet') {
return { httpGet: { path, port, scheme: 'HTTP' } }
}
if (type === 'tcpSocket') {
return { tcpSocket: { port } }
}
return { exec: { command: ['cat', '/tmp/healthy'] } }
}
if (config.enableLivenessProbe) {
container.livenessProbe = {
...probeConfig(config.livenessProbeType, config.livenessPath, config.livenessPort),
initialDelaySeconds: 15,
periodSeconds: 20,
timeoutSeconds: 5,
failureThreshold: 3,
}
}
if (config.enableReadinessProbe) {
container.readinessProbe = {
...probeConfig(config.readinessProbeType, config.readinessPath, config.readinessPort),
initialDelaySeconds: 5,
periodSeconds: 10,
timeoutSeconds: 3,
failureThreshold: 3,
}
}
const deployment = {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name: config.appName,
namespace: config.namespace,
labels,
},
spec: {
replicas: config.replicas,
selector: {
matchLabels: { app: labels.app },
},
strategy: config.strategy === 'RollingUpdate'
? {
type: 'RollingUpdate',
rollingUpdate: {
maxSurge: config.maxSurge,
maxUnavailable: config.maxUnavailable,
},
}
: { type: 'Recreate' },
template: {
metadata: { labels },
spec: {
containers: [container],
},
},
},
}
const header = `# K8s Deployment - 由 ConfTemplate 生成
# 生成时间: ${new Date().toLocaleString('zh-CN')}
# 部署命令: kubectl apply -f ${config.fileName || 'deployment.yaml'}
`
return header + '\n' + toYaml(deployment)
}
+234 -33
View File
@@ -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,50 +125,244 @@ 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)
// ======================== YAML 生成器 ========================
for (const [key, value] of Object.entries(obj)) {
function toYaml(obj, indent) {
indent = indent || 0
var lines = []
var prefix = ''
for (var p = 0; p < indent; p++) prefix += ' '
var keys = Object.keys(obj)
for (var ki = 0; ki < keys.length; ki++) {
var key = keys[ki]
var value = obj[key]
if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`)
for (const item of value) {
lines.push(prefix + key + ':')
for (var ai = 0; ai < value.length; ai++) {
var item = value[ai]
if (typeof item === 'object' && item !== null) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue
const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
}
lines.push(renderArrayObject(item, indent + 1))
} else {
lines.push(`${prefix} - ${item}`)
lines.push(prefix + ' - ' + item)
}
}
} else if (typeof value === 'object') {
lines.push(`${prefix}${key}:`)
lines.push(prefix + key + ':')
lines.push(toYaml(value, indent + 1))
} else {
lines.push(`${prefix}${key}: ${value}`)
lines.push(prefix + key + ': ' + value)
}
}
return lines.join('\n')
}
function renderArrayObject(item, indent) {
var prefix = ''
for (var p = 0; p < indent; p++) prefix += ' '
var entries = Object.entries(item).filter(function(pair) { return pair[1] !== null && pair[1] !== undefined && pair[1] !== '' })
if (entries.length === 0) return ''
var lines = []
var first = entries[0]
var fk = first[0], fv = first[1]
if (Array.isArray(fv)) {
lines.push(prefix + '- ' + fk + ':')
for (var ai = 0; ai < fv.length; ai++) {
var arrItem = fv[ai]
if (typeof arrItem === 'object' && arrItem !== null) {
lines.push(renderArrayObject(arrItem, indent + 2))
} else {
lines.push(prefix + ' - ' + arrItem)
}
}
} else if (typeof fv === 'object' && fv !== null) {
lines.push(prefix + '- ' + fk + ':')
lines.push(toYaml(fv, indent + 2))
} else {
lines.push(prefix + '- ' + fk + ': ' + fv)
}
for (var i = 1; i < entries.length; i++) {
var k = entries[i][0], v = entries[i][1]
if (v === null || v === undefined || v === '') continue
if (Array.isArray(v)) {
lines.push(prefix + ' ' + k + ':')
for (var ai = 0; ai < v.length; ai++) {
var arrItem = v[ai]
if (typeof arrItem === 'object' && arrItem !== null) {
lines.push(renderArrayObject(arrItem, indent + 2))
} else {
lines.push(prefix + ' - ' + arrItem)
}
}
} else if (typeof v === 'object') {
lines.push(prefix + ' ' + k + ':')
lines.push(toYaml(v, indent + 2))
} else {
lines.push(prefix + ' ' + k + ': ' + v)
}
}
return lines.join('\n')
}
export function generateK8sJobYaml(config) {
const container = {
var container = {
name: config.name,
image: config.image,
imagePullPolicy: config.imagePullPolicy,
command: config.command.split(' '),
}
const job = {
// 容器端口
if (config.containerPort) {
container.ports = [{
name: config.portName || 'http',
containerPort: config.containerPort,
protocol: config.protocol || 'TCP',
}]
}
// 资源限制
container.resources = {
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
}
// 环境变量
if (config.enableEnv && config.envVars) {
container.env = config.envVars.split('\n').filter(Boolean).map(function(line) {
var idx = line.indexOf('=')
return { name: line.substring(0, idx).trim(), value: line.substring(idx + 1).trim() }
})
}
if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) {
container.envFrom = []
if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } })
if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } })
}
// 卷挂载
var volumes = []
if (config.enableVolumeMount) {
container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }]
var vol = { name: config.volumeName }
var vt = config.volumeType
if (vt === 'emptyDir') vol.emptyDir = {}
else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName }
else if (vt === 'configMap') vol.configMap = { name: config.configMapName }
else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName }
else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' }
volumes.push(vol)
}
// Security Context
if (config.enableSecurityContext) {
container.securityContext = {
runAsUser: config.runAsUser,
runAsGroup: config.runAsGroup,
runAsNonRoot: config.runAsNonRoot,
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
}
}
var podSpec = {
containers: [container],
restartPolicy: config.restartPolicy,
}
// ServiceAccount
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
// Volumes
if (volumes.length) podSpec.volumes = volumes
// fsGroup
if (config.enableSecurityContext && config.fsGroup) {
podSpec.securityContext = { fsGroup: config.fsGroup }
}
var job = {
apiVersion: 'batch/v1',
kind: 'Job',
metadata: {
@@ -163,24 +373,15 @@ 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`
var header = '# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ' + new Date().toLocaleString('zh-CN') + '\n# 部署命令: kubectl apply -f ' + (config.fileName || 'job.yaml') + '\n'
return header + '\n' + toYaml(job)
}
+400 -219
View File
@@ -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: [
{ 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',
},
], default: 'IfNotPresent' },
],
},
{
title: '容器端口',
fields: [
{
key: 'containerPort',
label: '容器端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
},
{
key: 'protocol',
label: '协议',
type: 'select',
options: [
{ 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',
},
], 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: [
{ 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: '留空则由调度器自动分配',
},
], default: 'Always' },
{ key: 'nodeName', label: '指定节点', type: 'text', placeholder: 'node-1', default: '', tip: '留空则由调度器自动分配' },
],
},
{
title: '标签',
fields: [
{
key: 'appLabel',
label: 'app 标签值',
type: 'text',
default: '',
tip: '留空则使用 Pod 名称',
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用 Pod 名称' },
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
],
},
{
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)
}
+357 -190
View File
@@ -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: [
{ 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 },
},
], 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: [
{ 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' } },
],
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]}`)
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 {
lines.push(`${prefix} - ${item}`)
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
}
} else if (typeof value === 'object') {
lines.push(`${prefix}${key}:`)
lines.push(toYaml(value, indent + 1))
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')
}
+458
View File
@@ -0,0 +1,458 @@
// RHEL / CentOS 7-8 ifcfg 风格网络配置
// 覆盖: CentOS 7, RHEL 7, CentOS 8/Stream 8, RHEL 8, Rocky 8, AlmaLinux 8
// 配置文件位于 /etc/sysconfig/network-scripts/ifcfg-<name>
export const networkRhelIfcfgSchema = {
id: 'network-rhel-ifcfg',
name: 'RHEL/CentOS ifcfg',
icon: 'Connection',
category: '网络配置',
description: 'RHEL/CentOS 7-8 ifcfg 网络脚本(含 Bond/Bridge/VLAN)',
format: 'conf',
fileName: 'ifcfg-eth0',
groups: [
{
title: '系统版本',
fields: [
{
key: 'osVersion',
label: '操作系统版本',
type: 'select',
options: [
{ label: 'CentOS 7', value: 'centos7' },
{ label: 'RHEL 7', value: 'rhel7' },
{ label: 'CentOS 8 / Stream 8', value: 'centos8' },
{ label: 'RHEL 8', value: 'rhel8' },
{ label: 'Rocky Linux 8 / AlmaLinux 8', value: 'el8-clone' },
],
default: 'centos7',
tip: '影响配置文件注释与建议说明,ifcfg 语法本身通用',
},
],
},
{
title: '配置类型',
fields: [
{
key: 'configType',
label: '配置类型',
type: 'select',
options: [
{ label: '单网卡 - 静态 IP', value: 'static' },
{ label: '单网卡 - DHCP', value: 'dhcp' },
{ label: 'Bond - 链路聚合', value: 'bond' },
{ label: 'Bridge - 桥接', value: 'bridge' },
{ label: 'VLAN - 子接口', value: 'vlan' },
],
default: 'static',
},
],
},
{
title: '主接口配置',
fields: [
{
key: 'interface',
label: '主接口名',
type: 'text',
default: 'eth0',
tip: 'ifcfg-<此名称>,例如 eth0 / ens33 / enp0s3',
},
{
key: 'bootproto',
label: '协议',
type: 'select',
options: [
{ label: 'static / none - 静态', value: 'none' },
{ label: 'dhcp - 自动获取', value: 'dhcp' },
],
default: 'none',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'ipaddr',
label: 'IP 地址',
type: 'text',
default: '192.168.1.10',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'prefix',
label: '子网前缀 (CIDR)',
type: 'number',
min: 1,
max: 32,
default: 24,
tip: '例如 24 = 255.255.255.0',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'gateway',
label: '默认网关',
type: 'text',
default: '192.168.1.1',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'dns1',
label: '主 DNS',
type: 'text',
default: '8.8.8.8',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'dns2',
label: '备用 DNS',
type: 'text',
default: '8.8.4.4',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'routeMetric',
label: '路由 Metric',
type: 'number',
min: 0,
max: 9999,
default: 0,
tip: '默认路由优先级,值越小优先级越高;0 表示不输出',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'onboot',
label: '开机自动启用',
type: 'switch',
default: true,
},
{
key: 'ipv6',
label: '禁用 IPv6',
type: 'switch',
default: true,
tip: '在 ifcfg 中写入 IPV6INIT=no',
},
],
},
{
title: 'Bond 配置',
fields: [
{
key: 'bondName',
label: 'Bond 接口名',
type: 'text',
default: 'bond0',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'bondMode',
label: 'Bond 模式',
type: 'select',
options: [
{ label: '0 - balance-rr (轮询)', value: '0' },
{ label: '1 - active-backup (主备)', value: '1' },
{ label: '2 - balance-xor (XOR)', value: '2' },
{ label: '3 - broadcast (广播)', value: '3' },
{ label: '4 - 802.3ad (LACP 动态聚合)', value: '4' },
{ label: '5 - balance-tlb (自适应发送负载)', value: '5' },
{ label: '6 - balance-alb (自适应负载均衡)', value: '6' },
],
default: '4',
tip: '模式 4 需要交换机端开启 LACP;模式 1 兼容性最好',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'miimon',
label: 'MII 监控间隔 (ms)',
type: 'number',
min: 0,
max: 10000,
default: 100,
tip: '链路状态检测间隔,0 表示关闭',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'lacpRate',
label: 'LACP 速率',
type: 'select',
options: [
{ label: 'slow (默认 30s)', value: 'slow' },
{ label: 'fast (1s)', value: 'fast' },
],
default: 'fast',
tip: '仅 mode=4 (802.3ad) 生效',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'bondSlaves',
label: '成员网卡 (逗号分隔)',
type: 'text',
default: 'eth0,eth1',
tip: '会被设为 SLAVE=yesMASTER=<bondName>',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'primarySlave',
label: '主网卡 (可选)',
type: 'text',
default: '',
placeholder: '留空使用默认',
tip: '仅 mode=1 active-backup 时作为活动网卡,可留空',
dependsOn: { key: 'configType', value: 'bond' },
},
],
},
{
title: 'Bridge 配置',
fields: [
{
key: 'bridgeName',
label: 'Bridge 接口名',
type: 'text',
default: 'br0',
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgePorts',
label: '桥接成员 (逗号分隔)',
type: 'text',
default: 'eth0',
tip: '成员网卡会写成 BRIDGE=<bridgeName>',
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgeStp',
label: '启用 STP 生成树',
type: 'switch',
default: false,
dependsOn: { key: 'configType', value: 'bridge' },
},
],
},
{
title: 'VLAN 配置',
fields: [
{
key: 'vlanParent',
label: '父接口',
type: 'text',
default: 'eth0',
tip: '例如 eth0,生成的子接口为 eth0.<vlan_id>',
dependsOn: { key: 'configType', value: 'vlan' },
},
{
key: 'vlanId',
label: 'VLAN ID',
type: 'number',
min: 1,
max: 4094,
default: 100,
dependsOn: { key: 'configType', value: 'vlan' },
},
],
},
],
}
function genIfcfgFile(fileName, fields, header) {
const lines = []
if (header) lines.push(header)
for (const [k, v] of Object.entries(fields)) {
if (v === undefined || v === null) continue
lines.push(`${k}=${v}`)
}
return lines.join('\n')
}
export function generateNetworkRhelIfcfg(config) {
const out = []
const ts = new Date().toLocaleString('zh-CN')
out.push(`# =========================================================`)
out.push(`# RHEL/CentOS ifcfg 网络配置 - 由 ConfTemplate 生成`)
out.push(`# 生成时间: ${ts}`)
out.push(`# 目标目录: /etc/sysconfig/network-scripts/`)
out.push(`# 配置类型: ${config.configType}`)
out.push(`# =========================================================`)
out.push(``)
out.push(`# 应用步骤:`)
out.push(`# 1. 将下面每个 ifcfg-xxx 内容保存到对应文件`)
out.push(`# 2. 执行: systemctl restart network # CentOS 7`)
out.push(`# nmcli connection reload # CentOS 8+ (NetworkManager)`)
out.push(`# nmcli connection up <name>`)
out.push(``)
out.push(`# -----------------------------------------------------------`)
const ipv6Line = config.ipv6 ? 'IPV6INIT=no' : null
if (config.configType === 'static' || config.configType === 'dhcp') {
const bootproto = config.configType === 'dhcp' ? 'dhcp' : (config.bootproto || 'none')
const base = {
TYPE: 'Ethernet',
PROXY_METHOD: 'none',
BROWSER_ONLY: 'no',
BOOTPROTO: bootproto,
DEFROUTE: 'yes',
IPV4_FAILURE_FATAL: 'no',
NAME: config.interface,
DEVICE: config.interface,
ONBOOT: config.onboot ? 'yes' : 'no',
}
if (config.ipv6) base.IPV6INIT = 'no'
if (config.configType === 'static') {
Object.assign(base, {
IPADDR: config.ipaddr,
PREFIX: String(config.prefix),
GATEWAY: config.gateway,
DNS1: config.dns1,
DNS2: config.dns2,
})
if (config.routeMetric) base.METRIC = String(config.routeMetric)
}
out.push(``)
out.push(`# >>> 文件: ifcfg-${config.interface}`)
out.push(`# >>> 接口 ${config.interface} (${config.configType === 'dhcp' ? 'DHCP' : '静态 IP'})`)
out.push(genIfcfgFile(`ifcfg-${config.interface}`, base))
out.push(``)
}
if (config.configType === 'bond') {
// bond 主接口
const bondOpts = [`mode=${config.bondMode}`, `miimon=${config.miimon}`]
if (config.bondMode === '4') {
bondOpts.push(`lacp_rate=${config.lacpRate}`)
}
if (config.primarySlave) {
bondOpts.push(`primary=${config.primarySlave}`)
}
const master = {
TYPE: 'Bond',
BONDING_MASTER: 'yes',
PROXY_METHOD: 'none',
BROWSER_ONLY: 'no',
BOOTPROTO: config.bootproto || 'none',
DEFROUTE: 'yes',
IPV4_FAILURE_FATAL: 'no',
NAME: config.bondName,
DEVICE: config.bondName,
ONBOOT: config.onboot ? 'yes' : 'no',
BONDING_OPTS: `"${bondOpts.join(' ')}"`,
IPADDR: config.ipaddr,
PREFIX: String(config.prefix),
GATEWAY: config.gateway,
DNS1: config.dns1,
DNS2: config.dns2,
}
if (config.ipv6) master.IPV6INIT = 'no'
if (config.routeMetric) master.METRIC = String(config.routeMetric)
out.push(``)
out.push(`# >>> 文件: ifcfg-${config.bondName}`)
out.push(`# >>> Bond 主接口 ${config.bondName} (mode=${config.bondMode})`)
out.push(genIfcfgFile(`ifcfg-${config.bondName}`, master))
out.push(``)
// 成员接口
const slaves = String(config.bondSlaves || '').split(',').map((s) => s.trim()).filter(Boolean)
for (const slv of slaves) {
const slave = {
TYPE: 'Ethernet',
PROXY_METHOD: 'none',
BROWSER_ONLY: 'no',
BOOTPROTO: 'none',
DEFROUTE: 'no',
IPV4_FAILURE_FATAL: 'no',
NAME: slv,
DEVICE: slv,
ONBOOT: config.onboot ? 'yes' : 'no',
MASTER: config.bondName,
SLAVE: 'yes',
}
if (config.ipv6) slave.IPV6INIT = 'no'
out.push(`# >>> 文件: ifcfg-${slv}`)
out.push(`# >>> Bond 成员 ${slv}${config.bondName}`)
out.push(genIfcfgFile(`ifcfg-${slv}`, slave))
out.push(``)
}
out.push(`# 提示: mode=4 (LACP) 需要在交换机端开启 LACP 动态聚合`)
}
if (config.configType === 'bridge') {
// bridge 主接口
const br = {
TYPE: 'Bridge',
PROXY_METHOD: 'none',
BROWSER_ONLY: 'no',
BOOTPROTO: config.bootproto || 'none',
DEFROUTE: 'yes',
IPV4_FAILURE_FATAL: 'no',
NAME: config.bridgeName,
DEVICE: config.bridgeName,
ONBOOT: config.onboot ? 'yes' : 'no',
STP: config.bridgeStp ? 'yes' : 'no',
IPADDR: config.ipaddr,
PREFIX: String(config.prefix),
GATEWAY: config.gateway,
DNS1: config.dns1,
DNS2: config.dns2,
}
if (config.ipv6) br.IPV6INIT = 'no'
if (config.routeMetric) br.METRIC = String(config.routeMetric)
out.push(``)
out.push(`# >>> 文件: ifcfg-${config.bridgeName}`)
out.push(`# >>> Bridge ${config.bridgeName} (STP=${config.bridgeStp ? 'on' : 'off'})`)
out.push(genIfcfgFile(`ifcfg-${config.bridgeName}`, br))
out.push(``)
// 成员接口
const ports = String(config.bridgePorts || '').split(',').map((s) => s.trim()).filter(Boolean)
for (const p of ports) {
const port = {
TYPE: 'Ethernet',
PROXY_METHOD: 'none',
BROWSER_ONLY: 'no',
BOOTPROTO: 'none',
DEFROUTE: 'no',
IPV4_FAILURE_FATAL: 'no',
NAME: p,
DEVICE: p,
ONBOOT: config.onboot ? 'yes' : 'no',
BRIDGE: config.bridgeName,
}
if (config.ipv6) port.IPV6INIT = 'no'
out.push(`# >>> 文件: ifcfg-${p}`)
out.push(`# >>> Bridge 成员 ${p}${config.bridgeName}`)
out.push(genIfcfgFile(`ifcfg-${p}`, port))
out.push(``)
}
}
if (config.configType === 'vlan') {
const subName = `${config.vlanParent}.${config.vlanId}`
const vlan = {
TYPE: 'Ethernet',
PROXY_METHOD: 'none',
BROWSER_ONLY: 'no',
BOOTPROTO: config.bootproto || 'none',
DEFROUTE: 'yes',
IPV4_FAILURE_FATAL: 'no',
NAME: subName,
DEVICE: subName,
ONBOOT: config.onboot ? 'yes' : 'no',
VLAN: 'yes',
IPADDR: config.ipaddr,
PREFIX: String(config.prefix),
GATEWAY: config.gateway,
DNS1: config.dns1,
DNS2: config.dns2,
}
if (config.ipv6) vlan.IPV6INIT = 'no'
if (config.routeMetric) vlan.METRIC = String(config.routeMetric)
out.push(``)
out.push(`# >>> 文件: ifcfg-${subName}`)
out.push(`# >>> VLAN 子接口 ${subName} (VLAN ID=${config.vlanId})`)
out.push(genIfcfgFile(`ifcfg-${subName}`, vlan))
out.push(``)
out.push(`# 提示: 父接口 ${config.vlanParent} 也需要 ONBOOT=yes 并处于 up 状态`)
out.push(`# 若使用基于接口名的隐式 VLAN,VLAN=yes 即可;`)
out.push(`# 若用显式 VLAN_ID,可增加: PHYSDEV=${config.vlanParent}, VLAN_ID=${config.vlanId}`)
}
return out.join('\n')
}
+430
View File
@@ -0,0 +1,430 @@
// RHEL / CentOS 9 NetworkManager keyfile 风格网络配置
// 覆盖: CentOS Stream 9, RHEL 9, Rocky Linux 9, AlmaLinux 9
// 配置文件位于 /etc/NetworkManager/system-connections/<name>.nmconnection
// 说明: RHEL 9 / CentOS Stream 9 已弃用 network-scripts (ifcfg-*),必须使用 NM keyfile
export const networkRhelNmKeyfileSchema = {
id: 'network-rhel-nmkeyfile',
name: 'RHEL/CentOS 9 NM',
icon: 'Connection',
category: '网络配置',
description: 'RHEL/CentOS 9 NetworkManager keyfile(含 Bond/Bridge/VLAN)',
format: 'ini',
fileName: 'connection.nmconnection',
groups: [
{
title: '系统版本',
fields: [
{
key: 'osVersion',
label: '操作系统版本',
type: 'select',
options: [
{ label: 'CentOS Stream 9', value: 'centos-stream9' },
{ label: 'RHEL 9', value: 'rhel9' },
{ label: 'Rocky Linux 9', value: 'rocky9' },
{ label: 'AlmaLinux 9', value: 'alma9' },
],
default: 'centos-stream9',
tip: '语法相同,仅影响注释与说明',
},
],
},
{
title: '配置类型',
fields: [
{
key: 'configType',
label: '配置类型',
type: 'select',
options: [
{ label: '单网卡 - 静态 IP', value: 'static' },
{ label: '单网卡 - DHCP', value: 'dhcp' },
{ label: 'Bond - 链路聚合', value: 'bond' },
{ label: 'Bridge - 桥接', value: 'bridge' },
{ label: 'VLAN - 子接口', value: 'vlan' },
],
default: 'static',
},
],
},
{
title: '主接口配置',
fields: [
{
key: 'interface',
label: '主接口名',
type: 'text',
default: 'eth0',
tip: 'nmconnection 文件名建议与接口名一致',
},
{
key: 'ipaddr',
label: 'IP 地址',
type: 'text',
default: '192.168.1.10',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'prefix',
label: '子网前缀 (CIDR)',
type: 'number',
min: 1,
max: 32,
default: 24,
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'gateway',
label: '默认网关',
type: 'text',
default: '192.168.1.1',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'dns',
label: 'DNS (逗号分隔)',
type: 'text',
default: '8.8.8.8,8.8.4.4',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'routeMetric',
label: '路由 Metric',
type: 'number',
min: 0,
max: 9999,
default: 0,
tip: '默认路由优先级,值越小优先级越高;0 表示不输出',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'autoconnect',
label: '开机自动启用',
type: 'switch',
default: true,
},
{
key: 'ipv6Method',
label: 'IPv6 方法',
type: 'select',
options: [
{ label: 'disabled - 禁用', value: 'disabled' },
{ label: 'auto - 自动', value: 'auto' },
{ label: 'ignore - 忽略', value: 'ignore' },
],
default: 'disabled',
},
],
},
{
title: 'Bond 配置',
fields: [
{
key: 'bondName',
label: 'Bond 接口名',
type: 'text',
default: 'bond0',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'bondMode',
label: 'Bond 模式',
type: 'select',
options: [
{ label: 'balance-rr (0)', value: 'balance-rr' },
{ label: 'active-backup (1)', value: 'active-backup' },
{ label: 'balance-xor (2)', value: 'balance-xor' },
{ label: 'broadcast (3)', value: 'broadcast' },
{ label: '802.3ad (4 LACP)', value: '802.3ad' },
{ label: 'balance-tlb (5)', value: 'balance-tlb' },
{ label: 'balance-alb (6)', value: 'balance-alb' },
],
default: '802.3ad',
tip: 'mode 4 需要交换机端开启 LACP',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'miimon',
label: 'MII 监控间隔 (ms)',
type: 'number',
min: 0,
max: 10000,
default: 100,
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'lacpRate',
label: 'LACP 速率',
type: 'select',
options: [
{ label: 'slow (30s)', value: 'slow' },
{ label: 'fast (1s)', value: 'fast' },
],
default: 'fast',
tip: '仅 802.3ad 模式生效',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'bondSlaves',
label: '成员网卡 (逗号分隔)',
type: 'text',
default: 'eth0,eth1',
dependsOn: { key: 'configType', value: 'bond' },
},
],
},
{
title: 'Bridge 配置',
fields: [
{
key: 'bridgeName',
label: 'Bridge 接口名',
type: 'text',
default: 'br0',
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgePorts',
label: '桥接成员 (逗号分隔)',
type: 'text',
default: 'eth0',
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgeStp',
label: '启用 STP',
type: 'switch',
default: false,
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgeHelloTime',
label: 'STP Hello 时间 (秒)',
type: 'number',
min: 1,
max: 10,
default: 2,
dependsOn: { key: 'configType', value: 'bridge' },
},
],
},
{
title: 'VLAN 配置',
fields: [
{
key: 'vlanParent',
label: '父接口',
type: 'text',
default: 'eth0',
dependsOn: { key: 'configType', value: 'vlan' },
},
{
key: 'vlanId',
label: 'VLAN ID',
type: 'number',
min: 1,
max: 4094,
default: 100,
dependsOn: { key: 'configType', value: 'vlan' },
},
],
},
],
}
function genKeyfile(sections, headerComment) {
const lines = []
if (headerComment) {
lines.push(headerComment)
}
for (const [section, kv] of Object.entries(sections)) {
lines.push(`[${section}]`)
for (const [k, v] of Object.entries(kv)) {
if (v === undefined || v === null || v === '') continue
lines.push(`${k}=${v}`)
}
lines.push('')
}
return lines.join('\n')
}
export function generateNetworkRhelNmKeyfile(config) {
const out = []
const ts = new Date().toLocaleString('zh-CN')
out.push(`# =========================================================`)
out.push(`# RHEL/CentOS 9 NetworkManager keyfile 配置 - 由 ConfTemplate 生成`)
out.push(`# 生成时间: ${ts}`)
out.push(`# 目标目录: /etc/NetworkManager/system-connections/`)
out.push(`# 配置类型: ${config.configType}`)
out.push(`# =========================================================`)
out.push(``)
out.push(`# 应用步骤:`)
out.push(`# 1. 将下面每个 <name>.nmconnection 内容保存到对应文件`)
out.push(`# 2. 设置权限: chmod 600 <name>.nmconnection`)
out.push(`# 3. 执行: nmcli connection reload`)
out.push(`# nmcli connection up <name>`)
out.push(``)
out.push(`# keyfile 必须有 600 权限,否则 NetworkManager 会忽略它`)
out.push(`# -----------------------------------------------------------`)
const autoStr = config.autoconnect ? 'true' : 'false'
const dnsList = String(config.dns || '').split(',').map((s) => s.trim()).filter(Boolean).join(';')
const dnsSuffix = dnsList ? `${dnsList};` : ''
if (config.configType === 'static' || config.configType === 'dhcp') {
const sections = {
connection: {
id: config.interface,
type: 'ethernet',
'interface-name': config.interface,
autoconnect: autoStr,
},
ipv4: {},
ipv6: { method: config.ipv6Method },
}
if (config.configType === 'dhcp') {
sections.ipv4.method = 'auto'
} else {
sections.ipv4.method = 'manual'
sections.ipv4.addresses = `${config.ipaddr}/${config.prefix}`
sections.ipv4.gateway = config.gateway
if (dnsSuffix) sections.ipv4.dns = dnsSuffix
if (config.routeMetric) sections.ipv4['route-metric'] = String(config.routeMetric)
}
out.push(``)
out.push(`# >>> 文件: ${config.interface}.nmconnection`)
out.push(`# >>> 接口 ${config.interface} (${config.configType === 'dhcp' ? 'DHCP' : '静态'})`)
out.push(genKeyfile(sections))
}
if (config.configType === 'bond') {
// bond 主接口
const bondSections = {
connection: {
id: config.bondName,
type: 'bond',
'interface-name': config.bondName,
autoconnect: autoStr,
},
bond: {
miimon: String(config.miimon),
mode: config.bondMode,
},
ipv4: {
method: 'manual',
addresses: `${config.ipaddr}/${config.prefix}`,
gateway: config.gateway,
dns: dnsSuffix,
},
ipv6: { method: config.ipv6Method },
}
if (config.routeMetric) bondSections.ipv4['route-metric'] = String(config.routeMetric)
if (config.bondMode === '802.3ad') {
bondSections.bond['lacp_rate'] = config.lacpRate
}
out.push(``)
out.push(`# >>> 文件: ${config.bondName}.nmconnection`)
out.push(`# >>> Bond 主接口 ${config.bondName} (mode=${config.bondMode})`)
out.push(genKeyfile(bondSections))
// 成员接口
const slaves = String(config.bondSlaves || '').split(',').map((s) => s.trim()).filter(Boolean)
for (const slv of slaves) {
const slaveSections = {
connection: {
id: slv,
type: 'ethernet',
'interface-name': slv,
master: config.bondName,
'slave-type': 'bond',
autoconnect: autoStr,
},
ipv4: { method: 'disabled' },
ipv6: { method: config.ipv6Method },
}
out.push(`# >>> 文件: ${slv}.nmconnection`)
out.push(`# >>> Bond 成员 ${slv}${config.bondName}`)
out.push(genKeyfile(slaveSections))
}
out.push(`# 提示: mode=802.3ad 需要在交换机端开启 LACP`)
}
if (config.configType === 'bridge') {
// bridge 主接口
const brSections = {
connection: {
id: config.bridgeName,
type: 'bridge',
'interface-name': config.bridgeName,
autoconnect: autoStr,
},
bridge: {
stp: config.bridgeStp ? 'yes' : 'no',
'hello-time': String(config.bridgeHelloTime),
},
ipv4: {
method: 'manual',
addresses: `${config.ipaddr}/${config.prefix}`,
gateway: config.gateway,
dns: dnsSuffix,
},
ipv6: { method: config.ipv6Method },
}
if (config.routeMetric) brSections.ipv4['route-metric'] = String(config.routeMetric)
out.push(``)
out.push(`# >>> 文件: ${config.bridgeName}.nmconnection`)
out.push(`# >>> Bridge ${config.bridgeName} (STP=${config.bridgeStp ? 'on' : 'off'})`)
out.push(genKeyfile(brSections))
// 成员接口
const ports = String(config.bridgePorts || '').split(',').map((s) => s.trim()).filter(Boolean)
for (const p of ports) {
const portSections = {
connection: {
id: p,
type: 'ethernet',
'interface-name': p,
master: config.bridgeName,
'slave-type': 'bridge',
autoconnect: autoStr,
},
ipv4: { method: 'disabled' },
ipv6: { method: config.ipv6Method },
}
out.push(`# >>> 文件: ${p}.nmconnection`)
out.push(`# >>> Bridge 成员 ${p}${config.bridgeName}`)
out.push(genKeyfile(portSections))
}
}
if (config.configType === 'vlan') {
const subName = `${config.vlanParent}.${config.vlanId}`
const vlanSections = {
connection: {
id: subName,
type: 'vlan',
'interface-name': subName,
autoconnect: autoStr,
},
vlan: {
parent: config.vlanParent,
id: String(config.vlanId),
},
ipv4: {
method: 'manual',
addresses: `${config.ipaddr}/${config.prefix}`,
gateway: config.gateway,
dns: dnsSuffix,
},
ipv6: { method: config.ipv6Method },
}
if (config.routeMetric) vlanSections.ipv4['route-metric'] = String(config.routeMetric)
out.push(``)
out.push(`# >>> 文件: ${subName}.nmconnection`)
out.push(`# >>> VLAN 子接口 ${subName} (VLAN ID=${config.vlanId})`)
out.push(genKeyfile(vlanSections))
}
return out.join('\n')
}
+331
View File
@@ -0,0 +1,331 @@
// Ubuntu Netplan 网络配置
// 覆盖: Ubuntu 18.04 / 20.04 / 22.04 / 24.04
// 配置文件位于 /etc/netplan/<name>.yaml
export const networkUbuntuNetplanSchema = {
id: 'network-ubuntu-netplan',
name: 'Ubuntu Netplan',
icon: 'Connection',
category: '网络配置',
description: 'Ubuntu Netplan 配置(含 Bond/Bridge/VLAN)',
format: 'yaml',
fileName: '01-netcfg.yaml',
groups: [
{
title: '系统版本',
fields: [
{
key: 'osVersion',
label: '操作系统版本',
type: 'select',
options: [
{ label: 'Ubuntu 18.04 LTS', value: '18.04' },
{ label: 'Ubuntu 20.04 LTS', value: '20.04' },
{ label: 'Ubuntu 22.04 LTS', value: '22.04' },
{ label: 'Ubuntu 24.04 LTS', value: '24.04' },
],
default: '22.04',
tip: '18.04 推荐 renderer: networkd20.04+ 同样支持',
},
{
key: 'renderer',
label: '渲染器',
type: 'select',
options: [
{ label: 'networkd - systemd-networkd', value: 'networkd' },
{ label: 'NetworkManager', value: 'NetworkManager' },
],
default: 'networkd',
tip: '服务器默认 networkd;桌面版可改 NetworkManager',
},
],
},
{
title: '配置类型',
fields: [
{
key: 'configType',
label: '配置类型',
type: 'select',
options: [
{ label: '单网卡 - 静态 IP', value: 'static' },
{ label: '单网卡 - DHCP', value: 'dhcp' },
{ label: 'Bond - 链路聚合', value: 'bond' },
{ label: 'Bridge - 桥接', value: 'bridge' },
{ label: 'VLAN - 子接口', value: 'vlan' },
],
default: 'static',
},
],
},
{
title: '主接口配置',
fields: [
{
key: 'interface',
label: '主接口名',
type: 'text',
default: 'eth0',
tip: 'Ubuntu 18.04 常见 ens33 / enp0s320.04+ 已恢复 eth0',
},
{
key: 'ipaddr',
label: 'IP 地址',
type: 'text',
default: '192.168.1.10',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'prefix',
label: '子网前缀 (CIDR)',
type: 'number',
min: 1,
max: 32,
default: 24,
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'gateway',
label: '默认网关',
type: 'text',
default: '192.168.1.1',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'dns',
label: 'DNS (逗号分隔)',
type: 'text',
default: '8.8.8.8,8.8.4.4',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
{
key: 'routeMetric',
label: '路由 Metric',
type: 'number',
min: 0,
max: 9999,
default: 0,
tip: '默认路由优先级,值越小优先级越高;0 表示不输出',
dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] },
},
],
},
{
title: 'Bond 配置',
fields: [
{
key: 'bondName',
label: 'Bond 接口名',
type: 'text',
default: 'bond0',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'bondMode',
label: 'Bond 模式',
type: 'select',
options: [
{ label: 'balance-rr (0)', value: 'balance-rr' },
{ label: 'active-backup (1)', value: 'active-backup' },
{ label: 'balance-xor (2)', value: 'balance-xor' },
{ label: 'broadcast (3)', value: 'broadcast' },
{ label: '802.3ad (4 LACP)', value: '802.3ad' },
{ label: 'balance-tlb (5)', value: 'balance-tlb' },
{ label: 'balance-alb (6)', value: 'balance-alb' },
],
default: '802.3ad',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'miimon',
label: 'MII 监控间隔 (ms)',
type: 'number',
min: 0,
max: 10000,
default: 100,
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'lacpRate',
label: 'LACP 速率',
type: 'select',
options: [
{ label: 'slow (30s)', value: 'slow' },
{ label: 'fast (1s)', value: 'fast' },
],
default: 'fast',
dependsOn: { key: 'configType', value: 'bond' },
},
{
key: 'bondSlaves',
label: '成员网卡 (逗号分隔)',
type: 'text',
default: 'eth0,eth1',
dependsOn: { key: 'configType', value: 'bond' },
},
],
},
{
title: 'Bridge 配置',
fields: [
{
key: 'bridgeName',
label: 'Bridge 接口名',
type: 'text',
default: 'br0',
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgePorts',
label: '桥接成员 (逗号分隔)',
type: 'text',
default: 'eth0',
dependsOn: { key: 'configType', value: 'bridge' },
},
{
key: 'bridgeStp',
label: '启用 STP',
type: 'switch',
default: false,
dependsOn: { key: 'configType', value: 'bridge' },
},
],
},
{
title: 'VLAN 配置',
fields: [
{
key: 'vlanParent',
label: '父接口',
type: 'text',
default: 'eth0',
dependsOn: { key: 'configType', value: 'vlan' },
},
{
key: 'vlanId',
label: 'VLAN ID',
type: 'number',
min: 1,
max: 4094,
default: 100,
dependsOn: { key: 'configType', value: 'vlan' },
},
],
},
],
}
// 生成接口下的 IPv4 静态配置块(addresses / routes / nameservers)
// base = 接口下属性的缩进空格数(如接口在 2 空格则 base=4)
function ipBlock(base, extraLines) {
const pad = (n) => ' '.repeat(base + n)
const lines = []
lines.push(`${pad(0)}addresses:`)
lines.push(`${pad(2)}- ${this.cidr}`)
if (this.gateway) {
lines.push(`${pad(0)}routes:`)
lines.push(`${pad(2)}- to: default`)
lines.push(`${pad(4)}via: ${this.gateway}`)
if (this.routeMetric) {
lines.push(`${pad(4)}metric: ${this.routeMetric}`)
}
}
if (this.dnsList.length > 0) {
lines.push(`${pad(0)}nameservers:`)
lines.push(`${pad(2)}addresses:`)
for (const d of this.dnsList) lines.push(`${pad(4)}- ${d}`)
}
return lines.concat(extraLines || [])
}
export function generateNetworkUbuntuNetplan(config) {
const dnsList = String(config.dns || '').split(',').map((s) => s.trim()).filter(Boolean)
const cidr = `${config.ipaddr}/${config.prefix}`
const ts = new Date().toLocaleString('zh-CN')
// 绑定 ipBlock 的 this 上下文
const ctx = { cidr, gateway: config.gateway, dnsList, routeMetric: config.routeMetric }
const genIp = (base, extra) => ipBlock.call(ctx, base, extra)
const body = [] // 每个元素是一行,已包含完整缩进
if (config.configType === 'static') {
body.push(` ethernets:`)
body.push(` ${config.interface}:`)
body.push(...genIp(6))
} else if (config.configType === 'dhcp') {
body.push(` ethernets:`)
body.push(` ${config.interface}:`)
body.push(` dhcp4: true`)
if (dnsList.length > 0) {
body.push(` nameservers:`)
body.push(` addresses:`)
for (const d of dnsList) body.push(` - ${d}`)
}
} else if (config.configType === 'bond') {
const slaves = String(config.bondSlaves || '').split(',').map((s) => s.trim()).filter(Boolean)
body.push(` ethernets:`)
for (const slv of slaves) {
body.push(` ${slv}:`)
body.push(` dhcp4: false`)
body.push(` optional: true`)
}
body.push(` bonds:`)
body.push(` ${config.bondName}:`)
body.push(` interfaces:`)
for (const slv of slaves) body.push(` - ${slv}`)
body.push(...genIp(6, [
` parameters:`,
` mode: ${config.bondMode}`,
` miimon: ${config.miimon}`,
...(config.bondMode === '802.3ad' ? [` lacp-rate: ${config.lacpRate}`] : []),
]))
} else if (config.configType === 'bridge') {
const ports = String(config.bridgePorts || '').split(',').map((s) => s.trim()).filter(Boolean)
body.push(` ethernets:`)
for (const p of ports) {
body.push(` ${p}:`)
body.push(` dhcp4: false`)
body.push(` optional: true`)
}
body.push(` bridges:`)
body.push(` ${config.bridgeName}:`)
body.push(` interfaces:`)
for (const p of ports) body.push(` - ${p}`)
body.push(...genIp(6, [
` parameters:`,
` stp: ${config.bridgeStp ? 'true' : 'false'}`,
]))
} else if (config.configType === 'vlan') {
const subName = `${config.vlanParent}.${config.vlanId}`
body.push(` ethernets:`)
body.push(` ${config.vlanParent}:`)
body.push(` dhcp4: false`)
body.push(` optional: true`)
body.push(` vlans:`)
body.push(` ${subName}:`)
body.push(...genIp(6, [
` id: ${config.vlanId}`,
` link: ${config.vlanParent}`,
]))
}
const header = []
header.push(`# =========================================================`)
header.push(`# Ubuntu Netplan 配置 - 由 ConfTemplate 生成`)
header.push(`# 生成时间: ${ts}`)
header.push(`# 目标文件: /etc/netplan/01-netcfg.yaml`)
header.push(`# 配置类型: ${config.configType}`)
header.push(`# =========================================================`)
header.push(`#`)
header.push(`# 应用步骤:`)
header.push(`# 1. 将本文件保存到 /etc/netplan/01-netcfg.yaml`)
header.push(`# 2. 权限: sudo chmod 600 /etc/netplan/01-netcfg.yaml`)
header.push(`# 3. 应用: sudo netplan apply`)
header.push(`# 4. 验证: ip a / networkctl status`)
header.push(`#`)
header.push(`# 注意: netplan 文件需 600 权限,否则 apply 会拒绝执行`)
header.push(``)
return header.join('\n') + `network:\n version: 2\n renderer: ${config.renderer}\n` + body.join('\n') + '\n'
}
+173 -28
View File
@@ -192,11 +192,112 @@ 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 },
},
],
},
],
}
export function generateNginxConf(config) {
const lines = []
const ind = (n) => ' '.repeat(n) // 4-space indent helper
lines.push(`# NGINX 配置文件 - 由 ConfTemplate 生成`)
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
lines.push(``)
@@ -244,22 +345,68 @@ export function generateNginxConf(config) {
lines.push(``)
}
lines.push(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`)
lines.push(` # upstream backend {`)
lines.push(` # server 127.0.0.1:8080;`)
lines.push(` # }`)
// 限流配置
if (config.enableLimitReq) {
lines.push(` # 限流配置`)
lines.push(` limit_req_zone ${config.limitReqZone};`)
lines.push(``)
}
// 缓存配置
if (config.enableCache) {
lines.push(` # 代理缓存配置`)
lines.push(` proxy_cache_path ${config.cachePath};`)
lines.push(``)
}
// Upstream 负载均衡
if (config.enableUpstream) {
lines.push(` # Upstream 负载均衡`)
lines.push(` upstream ${config.upstreamName} {`)
if (config.upstreamMethod && config.upstreamMethod !== 'round-robin') {
lines.push(` ${config.upstreamMethod};`)
}
const servers = config.upstreamServers.split('\n').filter(s => s.trim())
servers.forEach(server => {
// 自动补充分号:用户输入的多行 server 指令若漏了 ; 也能正常工作
const line = server.trim()
lines.push(` ${line}${line.endsWith(';') ? '' : ';'}`)
})
lines.push(` }`)
lines.push(``)
}
// ============================================================
// HTTP → HTTPS 跳转 server (只有开启 HTTPS 时生成)
// ============================================================
if (config.enableHttps) {
lines.push(` # HTTP 自动跳转 HTTPS`)
lines.push(` server {`)
lines.push(` listen ${config.httpPort};`)
lines.push(` server_name ${config.serverName};`)
lines.push(` return 301 https://$host$request_uri;`)
lines.push(` }`)
lines.push(``)
}
// ============================================================
// 主 server 块(HTTP 或 HTTPS
// ============================================================
lines.push(` server {`)
// 监听指令:HTTP 端口始终监听(让 80 也能直接服务于不跳转场景)
if (config.enableHttps) {
lines.push(` listen ${config.httpsPort} ssl http2;`)
// HTTPS 场景:主 server 只监听 443HTTP 由上面跳转 server 处理
lines.push(` listen ${config.httpsPort} ssl;`)
lines.push(` http2 on;`)
} else {
lines.push(` listen ${config.httpPort};`)
}
lines.push(` server_name ${config.serverName};`)
lines.push(``)
// SSL 配置
if (config.enableHttps) {
lines.push(` # SSL 证书配置`)
lines.push(` ssl_certificate ${config.sslCertificate};`)
@@ -270,25 +417,9 @@ export function generateNginxConf(config) {
lines.push(` ssl_session_cache shared:SSL:10m;`)
lines.push(` ssl_session_timeout 10m;`)
lines.push(``)
// HTTP to HTTPS redirect
lines.push(` # HTTP 自动跳转 HTTPS`)
lines.push(` }`)
lines.push(``)
lines.push(` server {`)
lines.push(` listen 80;`)
lines.push(` server_name ${config.serverName};`)
lines.push(` return 301 https://$host$request_uri;`)
lines.push(` }`)
lines.push(``)
lines.push(` server {`)
lines.push(` listen ${config.httpsPort} ssl http2;`)
lines.push(` server_name ${config.serverName};`)
lines.push(` ssl_certificate ${config.sslCertificate};`)
lines.push(` ssl_certificate_key ${config.sslCertificateKey};`)
lines.push(``)
}
// CORS 配置(写在 server 作用域,全部带 always 确保 4xx/5xx 也带 CORS 头)
if (config.enableCors) {
lines.push(` # 跨域配置 (CORS)`)
lines.push(` add_header 'Access-Control-Allow-Origin' '*' always;`)
@@ -296,22 +427,28 @@ export function generateNginxConf(config) {
lines.push(` add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;`)
lines.push(` add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;`)
lines.push(``)
lines.push(` # OPTIONS 预检直接返回 204if 块内只放 return 不加 add_header`)
lines.push(` # 兼容 nginx 1.22+if 中 add_header 会被拒);`)
lines.push(` # CORS 头由 server 作用域的 always 继承到 204 响应。`)
lines.push(` if ($request_method = 'OPTIONS') {`)
lines.push(` add_header 'Access-Control-Allow-Origin' '*';`)
lines.push(` add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';`)
lines.push(` add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';`)
lines.push(` add_header 'Access-Control-Max-Age' 1728000;`)
lines.push(` add_header 'Content-Type' 'text/plain; charset=utf-8';`)
lines.push(` add_header 'Content-Length' 0;`)
lines.push(` return 204;`)
lines.push(` }`)
lines.push(``)
}
// 反向代理 location
lines.push(` location / {`)
if (config.enableLimitReq) {
lines.push(` limit_req zone=one burst=${config.limitReqBurst} nodelay;`)
}
if (config.enableProxy) {
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 +457,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 always;`)
}
} else {
lines.push(` root /usr/share/nginx/html;`)
lines.push(` index index.html index.htm;`)
+134
View File
@@ -180,6 +180,114 @@ export const postgresqlSchema = {
},
],
},
{
title: '连接池',
fields: [
{
key: 'superuserReservedConnections',
label: '超级用户保留连接数',
type: 'number',
min: 0,
max: 100,
default: 3,
tip: '为超级用户保留的连接数,确保管理员总能连入',
},
{
key: 'idleInTransactionSessionTimeout',
label: '空闲事务超时 (毫秒)',
type: 'number',
min: 0,
max: 86400000,
default: 0,
tip: '0 = 禁用。空闲事务超过此时间将被终止,防止连接泄漏',
},
],
},
{
title: '流复制',
fields: [
{
key: 'enableReplication',
label: '启用流复制',
type: 'switch',
default: false,
tip: '配置此实例支持流复制(主库或备库)',
},
{
key: 'hotStandby',
label: '热备模式',
type: 'switch',
default: true,
tip: '备库是否允许只读查询',
dependsOn: { key: 'enableReplication', value: true },
},
{
key: 'synchronousCommit',
label: '同步提交模式',
type: 'select',
options: [
{ label: 'on - 等待 WAL 写入本地磁盘 (默认)', value: 'on' },
{ label: 'remote_write - 等待备库写入 WAL', value: 'remote_write' },
{ label: 'off - 异步提交,性能最高', value: 'off' },
],
default: 'on',
tip: '同步提交级别,影响数据安全与性能',
dependsOn: { key: 'enableReplication', value: true },
},
],
},
{
title: 'Autovacuum',
fields: [
{
key: 'enableAutovacuum',
label: '启用 Autovacuum',
type: 'switch',
default: true,
tip: '自动清理死元组并更新统计信息',
},
{
key: 'autovacuumMaxWorkers',
label: '最大工作进程数',
type: 'number',
min: 1,
max: 10,
default: 3,
tip: '并行执行 autovacuum 的最大进程数',
dependsOn: { key: 'enableAutovacuum', value: true },
},
{
key: 'autovacuumNaptime',
label: '休眠间隔 (秒)',
type: 'number',
min: 1,
max: 86400,
default: 60,
tip: 'autovacuum 守护进程两次运行之间的休眠时间',
dependsOn: { key: 'enableAutovacuum', value: true },
},
{
key: 'autovacuumVacuumThreshold',
label: 'VACUUM 阈值',
type: 'number',
min: 0,
max: 10000,
default: 50,
tip: '触发 VACUUM 的最小死元组数量',
dependsOn: { key: 'enableAutovacuum', value: true },
},
{
key: 'autovacuumAnalyzeThreshold',
label: 'ANALYZE 阈值',
type: 'number',
min: 0,
max: 10000,
default: 50,
tip: '触发 ANALYZE 的最小变更行数',
dependsOn: { key: 'enableAutovacuum', value: true },
},
],
},
],
}
@@ -225,5 +333,31 @@ export function generatePostgresqlConf(config) {
lines.push(``)
}
// 连接池
lines.push(`# ======================== 连接池 ========================`)
lines.push(`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')
}
+152
View File
@@ -204,6 +204,117 @@ export const redisSchema = {
},
],
},
{
title: 'ACL 配置',
fields: [
{
key: 'enableAcl',
label: '启用 ACL',
type: 'switch',
default: false,
tip: '启用 Redis ACL 访问控制列表',
},
{
key: 'aclFile',
label: 'ACL 文件路径',
type: 'text',
default: '/etc/redis/users.acl',
tip: 'ACL 规则文件路径',
dependsOn: { key: 'enableAcl', value: true },
},
{
key: 'defaultUserPassword',
label: '默认用户密码',
type: 'text',
placeholder: '留空表示无密码',
default: '',
tip: '设置 default 用户的密码',
dependsOn: { key: 'enableAcl', value: true },
},
{
key: 'enableRenameCommand',
label: '重命名危险命令',
type: 'switch',
default: false,
tip: '重命名 FLUSHALL/FLUSHDB/CONFIG 命令以增强安全性',
dependsOn: { key: 'enableAcl', value: true },
},
],
},
{
title: '慢日志',
fields: [
{
key: 'slowlogLogSlowerThan',
label: '慢日志阈值 (微秒)',
type: 'number',
min: 0,
max: 10000000,
default: 10000,
tip: '执行时间超过此值(微秒)的命令将被记录,0 表示记录所有',
},
{
key: 'slowlogMaxLen',
label: '慢日志最大条数',
type: 'number',
min: 1,
max: 10000,
default: 128,
tip: '慢日志队列最大长度,超出后旧记录被丢弃',
},
],
},
{
title: '延迟监控',
fields: [
{
key: 'latencyMonitorThreshold',
label: '延迟监控阈值 (毫秒)',
type: 'number',
min: 0,
max: 86400000,
default: 0,
tip: '0 = 禁用延迟监控,设置大于 0 的值启用(单位毫秒)',
},
],
},
{
title: '客户端管理',
fields: [
{
key: 'maxmemorySamples',
label: '内存采样数量',
type: 'number',
min: 1,
max: 10,
default: 5,
tip: 'LRU/TTL 淘汰算法的采样数,越大越精确但越慢',
},
{
key: 'lazyfreeLazyEviction',
label: '惰性释放内存淘汰',
type: 'switch',
default: false,
tip: '内存淘汰时异步释放键,避免阻塞主线程',
},
{
key: 'lazyfreeLazyExpire',
label: '惰性释放过期键',
type: 'switch',
default: true,
tip: '过期键异步释放,避免阻塞主线程',
},
{
key: 'ioThreads',
label: 'IO 线程数',
type: 'number',
min: 1,
max: 8,
default: 1,
tip: '1 表示禁用多线程 IO,建议不超过 CPU 核心数',
},
],
},
],
}
@@ -314,6 +425,47 @@ export function generateRedisConf(config) {
lines.push(`tcp-keepalive ${config.tcpKeepalive}`)
lines.push(`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')
}
+4 -2
View File
@@ -230,8 +230,10 @@ export function generateSyslogConf(config) {
lines.push(`# ======================== 全局设置 ========================`)
lines.push(`$WorkDirectory /var/lib/rsyslog`)
lines.push(`$ActionFileDefaultTemplate ${config.logFormat}`)
if (config.enableHighPrecisionTimestamps) {
lines.push(`$ActionFileDefaultTemplate RSYSLOG_FileFormat`)
// 高精度时间戳:仅在 logFormat 不是 RSYSLOG_FileFormat 时追加自定义模板
// RSYSLOG_FileFormat 自带微秒精度,重复输出同名指令会让 rsyslog 报警告)
if (config.enableHighPrecisionTimestamps && config.logFormat !== 'RSYSLOG_FileFormat') {
lines.push(`template(name="HighPrecFmt" type="string" string="%timegenerated:::date-rfc3339%,%timegenerated:::date-subseconds% %HOSTNAME% %syslogtag%%msg%\\n")`)
}
lines.push(``)
+1
View File
@@ -5,6 +5,7 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
base: './',
plugins: [
vue(),
AutoImport({