Compare commits
8 Commits
3d6501efd5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ae7a5858f | |||
| 6426eedab5 | |||
| 4e98eb8c4a | |||
| e258969c37 | |||
| c45a59c6d1 | |||
| b951e29c00 | |||
| 14f02df58d | |||
| d80d33e6f6 |
@@ -3,3 +3,4 @@ dist/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.local
|
*.local
|
||||||
*.log
|
*.log
|
||||||
|
*.tar.gz
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🧩 支持组件(44 个)
|
## 🧩 支持组件(59 个)
|
||||||
|
|
||||||
| 分类 | 组件 | 输出格式 | 核心配置项 |
|
| 分类 | 组件 | 输出格式 | 核心配置项 |
|
||||||
|------|------|----------|------------|
|
|------|------|----------|------------|
|
||||||
@@ -85,6 +85,21 @@
|
|||||||
| 日志监控 | **Syslog** | `.conf` | UDP/TCP/RELP 协议、远程日志、按主机分目录、速率限制 |
|
| 日志监控 | **Syslog** | `.conf` | UDP/TCP/RELP 协议、远程日志、按主机分目录、速率限制 |
|
||||||
| 自动化运维 | **Ansible** | `.cfg` | SSH 连接、Pipelining、Become 提权、Forks、事实缓存 |
|
| 自动化运维 | **Ansible** | `.cfg` | SSH 连接、Pipelining、Become 提权、Forks、事实缓存 |
|
||||||
| 自动化运维 | **Ansible Playbook** | `.yaml` | 变量、包管理、服务管理、文件复制、模板渲染、Handlers |
|
| 自动化运维 | **Ansible Playbook** | `.yaml` | 变量、包管理、服务管理、文件复制、模板渲染、Handlers |
|
||||||
|
| 系统服务 | **NTP** | `.conf` | NTP 服务器、认证、轮询间隔、drift 文件 |
|
||||||
|
| 系统服务 | **DHCP** | `.conf` | 子网、地址范围、租约时间、网关、静态绑定 |
|
||||||
|
| 系统服务 | **BIND (DNS)** | `.conf` | 监听地址、转发器、递归、DNSSEC、Zone 配置 |
|
||||||
|
| 系统服务 | **NFS** | `.exports` | 共享路径、客户端地址、权限、root_squash、NFSv4 |
|
||||||
|
| 系统服务 | **SAMBA** | `.conf` | 工作组、共享目录、权限、用户认证、日志 |
|
||||||
|
| 系统服务 | **RSYNC** | `.conf` | 模块、路径、认证、主机白名单 |
|
||||||
|
| 系统服务 | **Sersync** | `.xml` | 监控目录、远程同步目标、过滤规则、定时触发 |
|
||||||
|
| 系统服务 | **vsftpd** | `.conf` | 被动模式、TLS、用户限制、chroot、日志 |
|
||||||
|
| 系统服务 | **Subversion** | `.conf` | 匿名访问、认证、SASL、Hook 脚本 |
|
||||||
|
| 数据库中间件 | **Mycat** | `.xml` | 端口、字符集、连接数、事务隔离、认证 |
|
||||||
|
| CI/CD | **GitLab** | `.rb` | 外部 URL、SSL、数据库、Redis、Registry、SMTP |
|
||||||
|
| CI/CD | **Jenkins** | `.yaml` | 端口、执行器、LDAP、Casc、Git 配置 |
|
||||||
|
| 高可用 | **Keepalived** | `.conf` | VRRP 实例、VIP、优先级、健康检查、通知脚本 |
|
||||||
|
| 高可用 | **HAProxy** | `.cfg` | 全局配置、前后端、负载均衡算法、SSL、统计页面 |
|
||||||
|
| 高可用 | **LVS** | `.conf` | 调度算法、VIP、Real Server、DR/NAT/TUN 模式 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+205
-4
@@ -1,21 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
<Sidebar v-model="activeSchemaId" />
|
<!-- 移动端顶部栏 -->
|
||||||
<div class="main-content">
|
<div class="mobile-header">
|
||||||
|
<button class="hamburger" @click="sidebarOpen = !sidebarOpen">
|
||||||
|
<el-icon :size="22"><Expand /></el-icon>
|
||||||
|
</button>
|
||||||
|
<div class="mobile-logo">
|
||||||
|
<h1>ConfTemplate</h1>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" type="info">{{ currentSchema.name }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 遮罩层 -->
|
||||||
|
<div
|
||||||
|
v-if="sidebarOpen"
|
||||||
|
class="sidebar-overlay"
|
||||||
|
@click="sidebarOpen = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<div class="sidebar-wrapper" :class="{ open: sidebarOpen }">
|
||||||
|
<Sidebar
|
||||||
|
v-model="activeSchemaId"
|
||||||
|
@select="sidebarOpen = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端 Tab 切换 -->
|
||||||
|
<div class="mobile-tabs">
|
||||||
|
<button
|
||||||
|
:class="{ active: activeTab === 'form' }"
|
||||||
|
@click="activeTab = 'form'"
|
||||||
|
>
|
||||||
|
<el-icon><Edit /></el-icon> 配置表单
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: activeTab === 'preview' }"
|
||||||
|
@click="activeTab = 'preview'"
|
||||||
|
>
|
||||||
|
<el-icon><Document /></el-icon> 代码预览
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="main-content" :class="{ 'mobile-hidden': activeTab !== 'form' }">
|
||||||
<ConfigForm
|
<ConfigForm
|
||||||
:key="activeSchemaId"
|
:key="activeSchemaId"
|
||||||
:schema="currentSchema"
|
:schema="currentSchema"
|
||||||
@update:config="onConfigUpdate"
|
@update:config="onConfigUpdate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="preview-content">
|
|
||||||
|
<div class="preview-content" :class="{ 'mobile-hidden': activeTab !== 'preview' }">
|
||||||
<CodePreview :code="generatedCode" :schema="currentSchema" />
|
<CodePreview :code="generatedCode" :schema="currentSchema" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { Expand, Edit, Document } from '@element-plus/icons-vue'
|
||||||
import Sidebar from './components/Sidebar.vue'
|
import Sidebar from './components/Sidebar.vue'
|
||||||
import ConfigForm from './components/ConfigForm.vue'
|
import ConfigForm from './components/ConfigForm.vue'
|
||||||
import CodePreview from './components/CodePreview.vue'
|
import CodePreview from './components/CodePreview.vue'
|
||||||
@@ -23,9 +67,18 @@ import { schemas, generators, getDefaultValues } from './schemas'
|
|||||||
|
|
||||||
const activeSchemaId = ref('nginx')
|
const activeSchemaId = ref('nginx')
|
||||||
const currentConfig = ref({})
|
const currentConfig = ref({})
|
||||||
|
const sidebarOpen = ref(false)
|
||||||
|
const activeTab = ref('form')
|
||||||
|
|
||||||
const currentSchema = computed(() => schemas[activeSchemaId.value])
|
const currentSchema = computed(() => schemas[activeSchemaId.value])
|
||||||
|
|
||||||
|
// 当 schema 切换时,立即用新 schema 的默认值重置 config
|
||||||
|
watch(activeSchemaId, (newId) => {
|
||||||
|
currentConfig.value = getDefaultValues(schemas[newId])
|
||||||
|
// 移动端切换组件后自动切到表单 tab
|
||||||
|
activeTab.value = 'form'
|
||||||
|
})
|
||||||
|
|
||||||
const generatedCode = computed(() => {
|
const generatedCode = computed(() => {
|
||||||
const gen = generators[activeSchemaId.value]
|
const gen = generators[activeSchemaId.value]
|
||||||
if (!gen) return ''
|
if (!gen) return ''
|
||||||
@@ -54,6 +107,7 @@ html, body, #app {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 桌面端布局 ===== */
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -75,6 +129,21 @@ html, body, #app {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端顶栏 - 默认隐藏 */
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层 - 默认隐藏 */
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端 Tab - 默认隐藏 */
|
||||||
|
.mobile-tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -106,4 +175,136 @@ html, body, #app {
|
|||||||
.el-select {
|
.el-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 平板端 (<= 1024px) ===== */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.main-content {
|
||||||
|
flex: 0 0 360px;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 移动端 (<= 768px) ===== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端顶栏 */
|
||||||
|
.mobile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #1a1a2e;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo h1 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层 */
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 199;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏变为抽屉 */
|
||||||
|
.sidebar-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: -280px;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 200;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-wrapper.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端 Tab */
|
||||||
|
.mobile-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #909399;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs button.active {
|
||||||
|
color: #667eea;
|
||||||
|
border-bottom-color: #667eea;
|
||||||
|
background: rgba(102, 126, 234, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区全宽 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
border-right: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏非活跃 tab */
|
||||||
|
.mobile-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
:key="itemId"
|
:key="itemId"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ active: modelValue === itemId }"
|
:class="{ active: modelValue === itemId }"
|
||||||
@click="$emit('update:modelValue', itemId)"
|
@click="$emit('update:modelValue', itemId); $emit('select')"
|
||||||
>
|
>
|
||||||
<el-icon><component :is="schemas[itemId].icon" /></el-icon>
|
<el-icon><component :is="schemas[itemId].icon" /></el-icon>
|
||||||
<div class="nav-item-info">
|
<div class="nav-item-info">
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<el-text size="small" type="info">v1.0.0 · 纯前端生成 · 44 个组件</el-text>
|
<el-text size="small" type="info">v1.0.0 · 纯前端生成 · 60 个组件</el-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -74,7 +74,7 @@ const props = defineProps({
|
|||||||
modelValue: String,
|
modelValue: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'select'])
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.config.errorHandler = (err, instance, info) => {
|
||||||
|
console.error('Vue Error:', err, info)
|
||||||
|
}
|
||||||
|
|
||||||
app.use(ElementPlus, { size: 'default' })
|
app.use(ElementPlus, { size: 'default' })
|
||||||
|
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
export const bindSchema = {
|
||||||
|
id: 'bind',
|
||||||
|
name: 'BIND (DNS)',
|
||||||
|
icon: 'Connection',
|
||||||
|
category: '系统服务',
|
||||||
|
description: 'BIND DNS 服务器配置',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'named.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '监听与查询',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'listenOn',
|
||||||
|
label: 'IPv4 监听地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '127.0.0.1; any',
|
||||||
|
tip: 'IPv4 监听地址列表',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'listenOnV6',
|
||||||
|
label: 'IPv6 监听地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '::1; any',
|
||||||
|
tip: 'IPv6 监听地址列表',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'allowQuery',
|
||||||
|
label: '允许查询',
|
||||||
|
type: 'text',
|
||||||
|
default: 'localhost; any',
|
||||||
|
tip: '允许哪些客户端查询',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'allowRecursion',
|
||||||
|
label: '允许递归',
|
||||||
|
type: 'text',
|
||||||
|
default: 'localhost; localnets',
|
||||||
|
tip: '允许哪些客户端进行递归查询',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'recursion',
|
||||||
|
label: '启用递归',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '转发配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'forwarders',
|
||||||
|
label: '转发服务器',
|
||||||
|
type: 'text',
|
||||||
|
default: '8.8.8.8; 8.8.4.4',
|
||||||
|
tip: '上游 DNS 服务器地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'forwardType',
|
||||||
|
label: '转发策略',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'first - 先转发再自行解析', value: 'first' },
|
||||||
|
{ label: 'only - 仅转发不自行解析', value: 'only' },
|
||||||
|
],
|
||||||
|
default: 'first',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '日志配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableLogging',
|
||||||
|
label: '启用日志',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logCategory',
|
||||||
|
label: '日志类别',
|
||||||
|
type: 'text',
|
||||||
|
default: 'queries',
|
||||||
|
dependsOn: { key: 'enableLogging', value: true },
|
||||||
|
tip: 'BIND 日志类别名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logFile',
|
||||||
|
label: '日志文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/log/named/queries.log',
|
||||||
|
dependsOn: { key: 'enableLogging', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DNSSEC',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableDnssec',
|
||||||
|
label: '启用 DNSSEC',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '启用 DNS 安全扩展',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dnssecValidation',
|
||||||
|
label: 'DNSSEC 验证',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '对查询结果进行 DNSSEC 验证',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '区域配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableZone',
|
||||||
|
label: '启用自定义区域',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '配置本地 DNS 区域',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zoneName',
|
||||||
|
label: '区域名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'example.com',
|
||||||
|
dependsOn: { key: 'enableZone', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zoneType',
|
||||||
|
label: '区域类型',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'master - 主区域', value: 'master' },
|
||||||
|
{ label: 'slave - 从区域', value: 'slave' },
|
||||||
|
{ label: 'hint - 根提示', value: 'hint' },
|
||||||
|
],
|
||||||
|
default: 'master',
|
||||||
|
dependsOn: { key: 'enableZone', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zoneFile',
|
||||||
|
label: '区域文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/named/example.com.zone',
|
||||||
|
dependsOn: { key: 'enableZone', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateBindConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`// BIND DNS 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`// 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Options block
|
||||||
|
lines.push(`// ======================== 全局选项 ========================`)
|
||||||
|
lines.push(`options {`)
|
||||||
|
lines.push(` listen-on port 53 { ${config.listenOn} };`)
|
||||||
|
lines.push(` listen-on-v6 port 53 { ${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(` recursion ${config.recursion ? 'yes' : 'no'};`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(` forwarders {`)
|
||||||
|
const forwarders = (config.forwarders || '').split(';').map((s) => s.trim()).filter(Boolean)
|
||||||
|
forwarders.forEach((f) => {
|
||||||
|
lines.push(` ${f};`)
|
||||||
|
})
|
||||||
|
lines.push(` };`)
|
||||||
|
lines.push(` forward ${config.forwardType};`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
if (config.enableDnssec) {
|
||||||
|
lines.push(` dnssec-enable yes;`)
|
||||||
|
}
|
||||||
|
if (config.dnssecValidation) {
|
||||||
|
lines.push(` dnssec-validation yes;`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(` pid-file "/run/named/named.pid";`)
|
||||||
|
lines.push(` session-keyfile "/run/named/session.key";`)
|
||||||
|
lines.push(`};`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
if (config.enableLogging) {
|
||||||
|
lines.push(`// ======================== 日志配置 ========================`)
|
||||||
|
lines.push(`logging {`)
|
||||||
|
lines.push(` channel default_debug {`)
|
||||||
|
lines.push(` file "data/named.run";`)
|
||||||
|
lines.push(` severity dynamic;`)
|
||||||
|
lines.push(` };`)
|
||||||
|
lines.push(` channel query_log {`)
|
||||||
|
lines.push(` file "${config.logFile}" versions 3 size 5m;`)
|
||||||
|
lines.push(` severity dynamic;`)
|
||||||
|
lines.push(` print-time yes;`)
|
||||||
|
lines.push(` print-category yes;`)
|
||||||
|
lines.push(` };`)
|
||||||
|
lines.push(` category ${config.logCategory} { query_log; };`)
|
||||||
|
lines.push(`};`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone configuration
|
||||||
|
lines.push(`// ======================== 标准区域 ========================`)
|
||||||
|
lines.push(`zone "." IN {`)
|
||||||
|
lines.push(` type hint;`)
|
||||||
|
lines.push(` file "named.ca";`)
|
||||||
|
lines.push(`};`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`zone "localhost" IN {`)
|
||||||
|
lines.push(` type master;`)
|
||||||
|
lines.push(` file "named.localhost";`)
|
||||||
|
lines.push(` allow-update { none; };`)
|
||||||
|
lines.push(`};`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`zone "0.0.127.in-addr.arpa" IN {`)
|
||||||
|
lines.push(` type master;`)
|
||||||
|
lines.push(` file "named.loopback";`)
|
||||||
|
lines.push(` allow-update { none; };`)
|
||||||
|
lines.push(`};`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
if (config.enableZone) {
|
||||||
|
lines.push(`// ======================== 自定义区域 ========================`)
|
||||||
|
lines.push(`zone "${config.zoneName}" IN {`)
|
||||||
|
lines.push(` type ${config.zoneType};`)
|
||||||
|
lines.push(` file "${config.zoneFile}";`)
|
||||||
|
if (config.zoneType === 'master') {
|
||||||
|
lines.push(` allow-update { none; };`)
|
||||||
|
}
|
||||||
|
lines.push(`};`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`include "/etc/named.rfc1912.zones";`)
|
||||||
|
lines.push(`include "/etc/named.root.key";`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
export const dhcpSchema = {
|
||||||
|
id: 'dhcp',
|
||||||
|
name: 'DHCP',
|
||||||
|
icon: 'Connection',
|
||||||
|
category: '系统服务',
|
||||||
|
description: 'ISC DHCP 服务器配置',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'dhcpd.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '全局参数',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'domainName',
|
||||||
|
label: '域名',
|
||||||
|
type: 'text',
|
||||||
|
default: 'example.com',
|
||||||
|
tip: '分配给客户端的域名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'domainNameServers',
|
||||||
|
label: 'DNS 服务器',
|
||||||
|
type: 'text',
|
||||||
|
default: '8.8.8.8, 8.8.4.4',
|
||||||
|
tip: '多个用逗号分隔',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultLeaseTime',
|
||||||
|
label: '默认租约时间 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 60,
|
||||||
|
max: 86400,
|
||||||
|
default: 600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxLeaseTime',
|
||||||
|
label: '最大租约时间 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 60,
|
||||||
|
max: 604800,
|
||||||
|
default: 7200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '子网配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'subnet',
|
||||||
|
label: '子网地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.0',
|
||||||
|
tip: 'DHCP 分配的子网',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'netmask',
|
||||||
|
label: '子网掩码',
|
||||||
|
type: 'text',
|
||||||
|
default: '255.255.255.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rangeStart',
|
||||||
|
label: '地址池起始',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.100',
|
||||||
|
tip: '动态分配范围起始 IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rangeEnd',
|
||||||
|
label: '地址池结束',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.200',
|
||||||
|
tip: '动态分配范围结束 IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gateway',
|
||||||
|
label: '默认网关',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'broadcastAddress',
|
||||||
|
label: '广播地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.255',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '静态主机绑定',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableStaticHost',
|
||||||
|
label: '启用静态主机',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '为特定 MAC 地址分配固定 IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'staticHostName',
|
||||||
|
label: '主机名',
|
||||||
|
type: 'text',
|
||||||
|
default: 'myhost',
|
||||||
|
dependsOn: { key: 'enableStaticHost', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'staticHostMac',
|
||||||
|
label: 'MAC 地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '00:11:22:33:44:55',
|
||||||
|
dependsOn: { key: 'enableStaticHost', value: true },
|
||||||
|
tip: '客户端 MAC 地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'staticHostIp',
|
||||||
|
label: '固定 IP 地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.50',
|
||||||
|
dependsOn: { key: 'enableStaticHost', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDhcpConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# DHCP 服务器配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Global parameters
|
||||||
|
lines.push(`# ======================== 全局参数 ========================`)
|
||||||
|
lines.push(`option domain-name "${config.domainName}";`)
|
||||||
|
const dnsServers = config.domainNameServers.split(',').map((s) => s.trim()).join(', ')
|
||||||
|
lines.push(`option domain-name-servers ${dnsServers};`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`default-lease-time ${config.defaultLeaseTime};`)
|
||||||
|
lines.push(`max-lease-time ${config.maxLeaseTime};`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`# 如果本机不是 DHCP 服务器的 authoritative,取消注释下行`)
|
||||||
|
lines.push(`# authoritative;`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Subnet configuration
|
||||||
|
lines.push(`# ======================== 子网配置 ========================`)
|
||||||
|
lines.push(`subnet ${config.subnet} netmask ${config.netmask} {`)
|
||||||
|
lines.push(` range ${config.rangeStart} ${config.rangeEnd};`)
|
||||||
|
lines.push(` option routers ${config.gateway};`)
|
||||||
|
lines.push(` option broadcast-address ${config.broadcastAddress};`)
|
||||||
|
lines.push(` option domain-name-servers ${dnsServers};`)
|
||||||
|
lines.push(` option domain-name "${config.domainName}";`)
|
||||||
|
lines.push(` default-lease-time ${config.defaultLeaseTime};`)
|
||||||
|
lines.push(` max-lease-time ${config.maxLeaseTime};`)
|
||||||
|
lines.push(`}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Static host
|
||||||
|
if (config.enableStaticHost) {
|
||||||
|
lines.push(`# ======================== 静态主机绑定 ========================`)
|
||||||
|
lines.push(`host ${config.staticHostName} {`)
|
||||||
|
lines.push(` hardware ethernet ${config.staticHostMac};`)
|
||||||
|
lines.push(` fixed-address ${config.staticHostIp};`)
|
||||||
|
lines.push(`}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
export const gitlabSchema = {
|
||||||
|
id: 'gitlab',
|
||||||
|
name: 'GitLab',
|
||||||
|
icon: 'Files',
|
||||||
|
category: 'CI/CD',
|
||||||
|
description: 'DevOps 生命周期平台,提供代码托管、CI/CD、制品管理等',
|
||||||
|
format: 'rb',
|
||||||
|
fileName: 'gitlab.rb',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '基础配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'externalUrl',
|
||||||
|
label: '外部访问 URL',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'http://gitlab.example.com',
|
||||||
|
default: 'http://gitlab.example.com',
|
||||||
|
required: true,
|
||||||
|
tip: 'GitLab 实例的外部访问地址',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Nginx 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'nginxListenPort',
|
||||||
|
label: 'Nginx 监听端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nginxEnableSsl',
|
||||||
|
label: '启用 SSL',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '为 Nginx 启用 HTTPS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sslCertificate',
|
||||||
|
label: 'SSL 证书路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/gitlab/ssl/gitlab.crt',
|
||||||
|
dependsOn: { key: 'nginxEnableSsl', value: true },
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sslCertificateKey',
|
||||||
|
label: 'SSL 私钥路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/gitlab/ssl/gitlab.key',
|
||||||
|
dependsOn: { key: 'nginxEnableSsl', value: true },
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据库配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'gitlabRbDbAdapter',
|
||||||
|
label: '数据库类型',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'PostgreSQL', value: 'postgresql' },
|
||||||
|
{ label: 'MySQL', value: 'mysql' },
|
||||||
|
],
|
||||||
|
default: 'postgresql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitlabRbDbHost',
|
||||||
|
label: '数据库主机',
|
||||||
|
type: 'text',
|
||||||
|
default: '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitlabRbDbPort',
|
||||||
|
label: '数据库端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 5432,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitlabRbDbName',
|
||||||
|
label: '数据库名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'gitlabhq_production',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitlabRbDbUser',
|
||||||
|
label: '数据库用户',
|
||||||
|
type: 'text',
|
||||||
|
default: 'gitlab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitlabRbDbPassword',
|
||||||
|
label: '数据库密码',
|
||||||
|
type: 'text',
|
||||||
|
default: 'changeme',
|
||||||
|
tip: '生产环境请使用强密码',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Redis 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'redisHost',
|
||||||
|
label: 'Redis 主机',
|
||||||
|
type: 'text',
|
||||||
|
default: '127.0.0.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'redisPort',
|
||||||
|
label: 'Redis 端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 6379,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Container Registry',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'registryEnable',
|
||||||
|
label: '启用 Container Registry',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: 'Docker 镜像仓库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'registryExternalUrl',
|
||||||
|
label: 'Registry 外部 URL',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'http://registry.example.com',
|
||||||
|
default: 'http://registry.example.com',
|
||||||
|
dependsOn: { key: 'registryEnable', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'GitLab Pages',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'pagesEnable',
|
||||||
|
label: '启用 GitLab Pages',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '静态网站托管服务',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pagesExternalUrl',
|
||||||
|
label: 'Pages 外部 URL',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'http://pages.example.com',
|
||||||
|
default: 'http://pages.example.com',
|
||||||
|
dependsOn: { key: 'pagesEnable', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SMTP 邮件配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'smtpEnable',
|
||||||
|
label: '启用 SMTP',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '用于发送邮件通知',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'smtpAddress',
|
||||||
|
label: 'SMTP 服务器',
|
||||||
|
type: 'text',
|
||||||
|
default: 'smtp.example.com',
|
||||||
|
dependsOn: { key: 'smtpEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'smtpPort',
|
||||||
|
label: 'SMTP 端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 587,
|
||||||
|
dependsOn: { key: 'smtpEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'smtpUser',
|
||||||
|
label: 'SMTP 用户名',
|
||||||
|
type: 'text',
|
||||||
|
default: 'gitlab@example.com',
|
||||||
|
dependsOn: { key: 'smtpEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'smtpPassword',
|
||||||
|
label: 'SMTP 密码',
|
||||||
|
type: 'text',
|
||||||
|
default: 'changeme',
|
||||||
|
dependsOn: { key: 'smtpEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'smtpTls',
|
||||||
|
label: '启用 TLS',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'smtpEnable', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备份与监控',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'backupKeepTime',
|
||||||
|
label: '备份保留时间 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
default: 604800,
|
||||||
|
tip: '604800 秒 = 7 天',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'prometheusEnable',
|
||||||
|
label: '启用 Prometheus 监控',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateGitlabRb(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# GitLab 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`## 外部访问 URL`)
|
||||||
|
lines.push(`external_url '${config.externalUrl}'`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Nginx
|
||||||
|
lines.push(`## Nginx 配置`)
|
||||||
|
lines.push(`nginx['listen_port'] = ${config.nginxListenPort}`)
|
||||||
|
if (config.nginxEnableSsl) {
|
||||||
|
lines.push(`nginx['ssl_certificate'] = '${config.sslCertificate}'`)
|
||||||
|
lines.push(`nginx['ssl_certificate_key'] = '${config.sslCertificateKey}'`)
|
||||||
|
} else {
|
||||||
|
lines.push(`nginx['enable_ssl'] = false`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Database
|
||||||
|
lines.push(`## 数据库配置`)
|
||||||
|
if (config.gitlabRbDbAdapter === 'postgresql') {
|
||||||
|
lines.push(`postgresql['enable'] = true`)
|
||||||
|
lines.push(`gitlab_rails['db_adapter'] = 'postgresql'`)
|
||||||
|
lines.push(`gitlab_rails['db_host'] = '${config.gitlabRbDbHost}'`)
|
||||||
|
lines.push(`gitlab_rails['db_port'] = ${config.gitlabRbDbPort}`)
|
||||||
|
} else {
|
||||||
|
lines.push(`postgresql['enable'] = false`)
|
||||||
|
lines.push(`gitlab_rails['db_adapter'] = 'mysql2'`)
|
||||||
|
lines.push(`gitlab_rails['db_host'] = '${config.gitlabRbDbHost}'`)
|
||||||
|
lines.push(`gitlab_rails['db_port'] = ${config.gitlabRbDbPort}`)
|
||||||
|
}
|
||||||
|
lines.push(`gitlab_rails['db_database'] = '${config.gitlabRbDbName}'`)
|
||||||
|
lines.push(`gitlab_rails['db_username'] = '${config.gitlabRbDbUser}'`)
|
||||||
|
lines.push(`gitlab_rails['db_password'] = '${config.gitlabRbDbPassword}'`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Redis
|
||||||
|
lines.push(`## Redis 配置`)
|
||||||
|
lines.push(`redis['enable'] = true`)
|
||||||
|
lines.push(`gitlab_rails['redis_host'] = '${config.redisHost}'`)
|
||||||
|
lines.push(`gitlab_rails['redis_port'] = ${config.redisPort}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Registry
|
||||||
|
if (config.registryEnable) {
|
||||||
|
lines.push(`## Container Registry`)
|
||||||
|
lines.push(`registry['enable'] = true`)
|
||||||
|
lines.push(`registry_external_url '${config.registryExternalUrl}'`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
if (config.pagesEnable) {
|
||||||
|
lines.push(`## GitLab Pages`)
|
||||||
|
lines.push(`pages_external_url '${config.pagesExternalUrl}'`)
|
||||||
|
lines.push(`gitlab_pages['enable'] = true`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMTP
|
||||||
|
if (config.smtpEnable) {
|
||||||
|
lines.push(`## SMTP 邮件配置`)
|
||||||
|
lines.push(`gitlab_rails['smtp_enable'] = true`)
|
||||||
|
lines.push(`gitlab_rails['smtp_address'] = '${config.smtpAddress}'`)
|
||||||
|
lines.push(`gitlab_rails['smtp_port'] = ${config.smtpPort}`)
|
||||||
|
lines.push(`gitlab_rails['smtp_user_name'] = '${config.smtpUser}'`)
|
||||||
|
lines.push(`gitlab_rails['smtp_password'] = '${config.smtpPassword}'`)
|
||||||
|
lines.push(`gitlab_rails['smtp_tls'] = ${config.smtpTls}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup
|
||||||
|
lines.push(`## 备份配置`)
|
||||||
|
lines.push(`gitlab_rails['backup_keep_time'] = ${config.backupKeepTime}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Prometheus
|
||||||
|
if (config.prometheusEnable) {
|
||||||
|
lines.push(`## Prometheus 监控`)
|
||||||
|
lines.push(`prometheus['enable'] = true`)
|
||||||
|
} else {
|
||||||
|
lines.push(`prometheus['enable'] = false`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
export const haproxySchema = {
|
||||||
|
id: 'haproxy',
|
||||||
|
name: 'HAProxy',
|
||||||
|
icon: 'Connection',
|
||||||
|
category: '高可用',
|
||||||
|
description: '高性能 TCP/HTTP 负载均衡器和代理服务器',
|
||||||
|
format: 'cfg',
|
||||||
|
fileName: 'haproxy.cfg',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: 'Global 全局配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'globalMaxconn',
|
||||||
|
label: '最大连接数',
|
||||||
|
type: 'number',
|
||||||
|
min: 100,
|
||||||
|
max: 1000000,
|
||||||
|
default: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'globalLog',
|
||||||
|
label: '日志输出',
|
||||||
|
type: 'text',
|
||||||
|
default: '127.0.0.1 local0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'globalChroot',
|
||||||
|
label: '启用 chroot',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '安全加固,限制进程访问范围',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'chrootDir',
|
||||||
|
label: 'chroot 目录',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/lib/haproxy',
|
||||||
|
dependsOn: { key: 'globalChroot', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'globalUser',
|
||||||
|
label: '运行用户',
|
||||||
|
type: 'text',
|
||||||
|
default: 'haproxy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'globalGroup',
|
||||||
|
label: '运行用户组',
|
||||||
|
type: 'text',
|
||||||
|
default: 'haproxy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'globalStatsEnable',
|
||||||
|
label: '启用 Stats Socket',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statsSocket',
|
||||||
|
label: 'Stats Socket 路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/run/haproxy.sock',
|
||||||
|
dependsOn: { key: 'globalStatsEnable', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Defaults 默认配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'defaultsMode',
|
||||||
|
label: '代理模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'HTTP 模式', value: 'http' },
|
||||||
|
{ label: 'TCP 模式', value: 'tcp' },
|
||||||
|
],
|
||||||
|
default: 'http',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsLog',
|
||||||
|
label: '日志模式',
|
||||||
|
type: 'text',
|
||||||
|
default: 'global',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsOptionHttplog',
|
||||||
|
label: '启用 HTTP 日志',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsOptionDontlognull',
|
||||||
|
label: '忽略空连接日志',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsTimeoutConnect',
|
||||||
|
label: '连接超时 (ms)',
|
||||||
|
type: 'number',
|
||||||
|
min: 100,
|
||||||
|
max: 60000,
|
||||||
|
default: 5000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsTimeoutClient',
|
||||||
|
label: '客户端超时 (ms)',
|
||||||
|
type: 'number',
|
||||||
|
min: 1000,
|
||||||
|
max: 300000,
|
||||||
|
default: 50000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsTimeoutServer',
|
||||||
|
label: '服务端超时 (ms)',
|
||||||
|
type: 'number',
|
||||||
|
min: 1000,
|
||||||
|
max: 300000,
|
||||||
|
default: 50000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultsRetries',
|
||||||
|
label: '重试次数',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
default: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Frontend 前端配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'frontendName',
|
||||||
|
label: '前端名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'myapp_front',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'frontendBind',
|
||||||
|
label: '绑定地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '*:80',
|
||||||
|
tip: '格式: 地址:端口',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'frontendBackend',
|
||||||
|
label: '关联后端',
|
||||||
|
type: 'text',
|
||||||
|
default: 'myapp_back',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enableSsl',
|
||||||
|
label: '启用 SSL',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sslBind',
|
||||||
|
label: 'SSL 绑定配置',
|
||||||
|
type: 'text',
|
||||||
|
default: '*:443 ssl crt /etc/haproxy/cert.pem',
|
||||||
|
dependsOn: { key: 'enableSsl', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Backend 后端配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'backendName',
|
||||||
|
label: '后端名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'myapp_back',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'balanceType',
|
||||||
|
label: '负载均衡算法',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'roundrobin - 轮询 (推荐)', value: 'roundrobin' },
|
||||||
|
{ label: 'leastconn - 最少连接', value: 'leastconn' },
|
||||||
|
{ label: 'source - 源地址哈希', value: 'source' },
|
||||||
|
{ label: 'uri - URI 哈希', value: 'uri' },
|
||||||
|
],
|
||||||
|
default: 'roundrobin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'backendServer1',
|
||||||
|
label: '后端服务器 1',
|
||||||
|
type: 'text',
|
||||||
|
default: 'web1 192.168.1.10:8080 check',
|
||||||
|
tip: '格式: 名称 IP:端口 check',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'backendServer2',
|
||||||
|
label: '后端服务器 2',
|
||||||
|
type: 'text',
|
||||||
|
default: 'web2 192.168.1.11:8080 check',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Stats 统计页面',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableStats',
|
||||||
|
label: '启用统计页面',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statsUri',
|
||||||
|
label: '统计页面 URI',
|
||||||
|
type: 'text',
|
||||||
|
default: '/haproxy?stats',
|
||||||
|
dependsOn: { key: 'enableStats', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statsAuthEnable',
|
||||||
|
label: '统计页面认证',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'enableStats', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statsUser',
|
||||||
|
label: '统计页面用户名',
|
||||||
|
type: 'text',
|
||||||
|
default: 'admin',
|
||||||
|
dependsOn: { key: 'statsAuthEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'statsPassword',
|
||||||
|
label: '统计页面密码',
|
||||||
|
type: 'text',
|
||||||
|
default: 'admin',
|
||||||
|
dependsOn: { key: 'statsAuthEnable', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateHaproxyCfg(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# HAProxy 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Global
|
||||||
|
lines.push(`global`)
|
||||||
|
lines.push(` log ${config.globalLog} local0`)
|
||||||
|
lines.push(` maxconn ${config.globalMaxconn}`)
|
||||||
|
lines.push(` user ${config.globalUser}`)
|
||||||
|
lines.push(` group ${config.globalGroup}`)
|
||||||
|
lines.push(` daemon`)
|
||||||
|
if (config.globalChroot) {
|
||||||
|
lines.push(` chroot ${config.chrootDir}`)
|
||||||
|
}
|
||||||
|
if (config.globalStatsEnable) {
|
||||||
|
lines.push(` stats socket ${config.statsSocket} mode 660 level admin`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
lines.push(`defaults`)
|
||||||
|
lines.push(` mode ${config.defaultsMode}`)
|
||||||
|
lines.push(` log ${config.defaultsLog}`)
|
||||||
|
if (config.defaultsOptionHttplog) {
|
||||||
|
lines.push(` option httplog`)
|
||||||
|
}
|
||||||
|
if (config.defaultsOptionDontlognull) {
|
||||||
|
lines.push(` option dontlognull`)
|
||||||
|
}
|
||||||
|
lines.push(` timeout connect ${config.defaultsTimeoutConnect}`)
|
||||||
|
lines.push(` timeout client ${config.defaultsTimeoutClient}`)
|
||||||
|
lines.push(` timeout server ${config.defaultsTimeoutServer}`)
|
||||||
|
lines.push(` retries ${config.defaultsRetries}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Frontend
|
||||||
|
lines.push(`frontend ${config.frontendName}`)
|
||||||
|
lines.push(` bind ${config.frontendBind}`)
|
||||||
|
if (config.enableSsl) {
|
||||||
|
lines.push(` bind ${config.sslBind}`)
|
||||||
|
}
|
||||||
|
lines.push(` default_backend ${config.frontendBackend}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Backend
|
||||||
|
lines.push(`backend ${config.backendName}`)
|
||||||
|
lines.push(` balance ${config.balanceType}`)
|
||||||
|
if (config.backendServer1) {
|
||||||
|
lines.push(` server ${config.backendServer1}`)
|
||||||
|
}
|
||||||
|
if (config.backendServer2) {
|
||||||
|
lines.push(` server ${config.backendServer2}`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
if (config.enableStats) {
|
||||||
|
lines.push(`listen stats`)
|
||||||
|
lines.push(` bind *:8404`)
|
||||||
|
lines.push(` stats enable`)
|
||||||
|
lines.push(` stats uri ${config.statsUri}`)
|
||||||
|
if (config.statsAuthEnable) {
|
||||||
|
lines.push(` stats auth ${config.statsUser}:${config.statsPassword}`)
|
||||||
|
}
|
||||||
|
lines.push(` stats refresh 10s`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
+87
-1
@@ -61,6 +61,7 @@ import { etcdSchema, generateEtcdYml } from './etcd'
|
|||||||
// ============ 容器化 ============
|
// ============ 容器化 ============
|
||||||
import { dockerSchema, generateDockerDaemonJson } from './docker'
|
import { dockerSchema, generateDockerDaemonJson } from './docker'
|
||||||
import { dockerComposeSchema, generateDockerComposeYml } from './docker-compose'
|
import { dockerComposeSchema, generateDockerComposeYml } from './docker-compose'
|
||||||
|
import { dockerfileSchema, generateDockerfile } from './dockerfile'
|
||||||
|
|
||||||
// ============ 日志监控 ============
|
// ============ 日志监控 ============
|
||||||
import { logstashSchema, generateLogstashConf } from './logstash'
|
import { logstashSchema, generateLogstashConf } from './logstash'
|
||||||
@@ -74,6 +75,29 @@ import { syslogSchema, generateSyslogConf } from './syslog'
|
|||||||
import { ansibleSchema, generateAnsibleCfg } from './ansible'
|
import { ansibleSchema, generateAnsibleCfg } from './ansible'
|
||||||
import { ansiblePlaybookSchema, generateAnsiblePlaybookYaml } from './ansible-playbook'
|
import { ansiblePlaybookSchema, generateAnsiblePlaybookYaml } from './ansible-playbook'
|
||||||
|
|
||||||
|
// ============ CI/CD ============
|
||||||
|
import { gitlabSchema, generateGitlabRb } from './gitlab'
|
||||||
|
import { jenkinsSchema, generateJenkinsYaml } from './jenkins'
|
||||||
|
|
||||||
|
// ============ 高可用 ============
|
||||||
|
import { keepalivedSchema, generateKeepalivedConf } from './keepalived'
|
||||||
|
import { haproxySchema, generateHaproxyCfg } from './haproxy'
|
||||||
|
import { lvsSchema, generateLvsConf } from './lvs'
|
||||||
|
|
||||||
|
// ============ 系统服务 ============
|
||||||
|
import { ntpSchema, generateNtpConf } from './ntp'
|
||||||
|
import { dhcpSchema, generateDhcpConf } from './dhcp'
|
||||||
|
import { bindSchema, generateBindConf } from './bind'
|
||||||
|
import { nfsSchema, generateNfsExports } from './nfs'
|
||||||
|
import { sambaSchema, generateSambaConf } from './samba'
|
||||||
|
import { rsyncSchema, generateRsyncConf } from './rsync'
|
||||||
|
import { sersyncSchema, generateSersyncXml } from './sersync'
|
||||||
|
import { vsftpdSchema, generateVsftpdConf } from './vsftpd'
|
||||||
|
import { subversionSchema, generateSubversionConf } from './subversion'
|
||||||
|
|
||||||
|
// ============ 数据库中间件 ============
|
||||||
|
import { mycatSchema, generateMycatServerXml } from './mycat'
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Schema Registry
|
// Schema Registry
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -129,6 +153,7 @@ export const schemas = {
|
|||||||
// 容器化
|
// 容器化
|
||||||
docker: dockerSchema,
|
docker: dockerSchema,
|
||||||
'docker-compose': dockerComposeSchema,
|
'docker-compose': dockerComposeSchema,
|
||||||
|
dockerfile: dockerfileSchema,
|
||||||
// 日志监控
|
// 日志监控
|
||||||
logstash: logstashSchema,
|
logstash: logstashSchema,
|
||||||
kibana: kibanaSchema,
|
kibana: kibanaSchema,
|
||||||
@@ -139,6 +164,25 @@ export const schemas = {
|
|||||||
// 自动化运维
|
// 自动化运维
|
||||||
ansible: ansibleSchema,
|
ansible: ansibleSchema,
|
||||||
'ansible-playbook': ansiblePlaybookSchema,
|
'ansible-playbook': ansiblePlaybookSchema,
|
||||||
|
// CI/CD
|
||||||
|
gitlab: gitlabSchema,
|
||||||
|
jenkins: jenkinsSchema,
|
||||||
|
// 高可用
|
||||||
|
keepalived: keepalivedSchema,
|
||||||
|
haproxy: haproxySchema,
|
||||||
|
lvs: lvsSchema,
|
||||||
|
// 系统服务
|
||||||
|
ntp: ntpSchema,
|
||||||
|
dhcp: dhcpSchema,
|
||||||
|
bind: bindSchema,
|
||||||
|
nfs: nfsSchema,
|
||||||
|
samba: sambaSchema,
|
||||||
|
rsync: rsyncSchema,
|
||||||
|
sersync: sersyncSchema,
|
||||||
|
vsftpd: vsftpdSchema,
|
||||||
|
subversion: subversionSchema,
|
||||||
|
// 数据库中间件
|
||||||
|
mycat: mycatSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -182,6 +226,7 @@ export const generators = {
|
|||||||
etcd: generateEtcdYml,
|
etcd: generateEtcdYml,
|
||||||
docker: generateDockerDaemonJson,
|
docker: generateDockerDaemonJson,
|
||||||
'docker-compose': generateDockerComposeYml,
|
'docker-compose': generateDockerComposeYml,
|
||||||
|
dockerfile: generateDockerfile,
|
||||||
logstash: generateLogstashConf,
|
logstash: generateLogstashConf,
|
||||||
kibana: generateKibanaYml,
|
kibana: generateKibanaYml,
|
||||||
prometheus: generatePrometheusYml,
|
prometheus: generatePrometheusYml,
|
||||||
@@ -190,6 +235,25 @@ export const generators = {
|
|||||||
syslog: generateSyslogConf,
|
syslog: generateSyslogConf,
|
||||||
ansible: generateAnsibleCfg,
|
ansible: generateAnsibleCfg,
|
||||||
'ansible-playbook': generateAnsiblePlaybookYaml,
|
'ansible-playbook': generateAnsiblePlaybookYaml,
|
||||||
|
// CI/CD
|
||||||
|
gitlab: generateGitlabRb,
|
||||||
|
jenkins: generateJenkinsYaml,
|
||||||
|
// 高可用
|
||||||
|
keepalived: generateKeepalivedConf,
|
||||||
|
haproxy: generateHaproxyCfg,
|
||||||
|
lvs: generateLvsConf,
|
||||||
|
// 系统服务
|
||||||
|
ntp: generateNtpConf,
|
||||||
|
dhcp: generateDhcpConf,
|
||||||
|
bind: generateBindConf,
|
||||||
|
nfs: generateNfsExports,
|
||||||
|
samba: generateSambaConf,
|
||||||
|
rsync: generateRsyncConf,
|
||||||
|
sersync: generateSersyncXml,
|
||||||
|
vsftpd: generateVsftpdConf,
|
||||||
|
subversion: generateSubversionConf,
|
||||||
|
// 数据库中间件
|
||||||
|
mycat: generateMycatServerXml,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -265,7 +329,7 @@ export const categories = [
|
|||||||
{
|
{
|
||||||
name: '容器化',
|
name: '容器化',
|
||||||
icon: 'Box',
|
icon: 'Box',
|
||||||
items: ['docker', 'docker-compose'],
|
items: ['docker', 'docker-compose', 'dockerfile'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '日志监控',
|
name: '日志监控',
|
||||||
@@ -277,6 +341,26 @@ export const categories = [
|
|||||||
icon: 'Files',
|
icon: 'Files',
|
||||||
items: ['ansible', 'ansible-playbook'],
|
items: ['ansible', 'ansible-playbook'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'CI/CD',
|
||||||
|
icon: 'Files',
|
||||||
|
items: ['gitlab', 'jenkins'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '高可用',
|
||||||
|
icon: 'Connection',
|
||||||
|
items: ['keepalived', 'haproxy', 'lvs'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '系统服务',
|
||||||
|
icon: 'Setting',
|
||||||
|
items: ['ntp', 'dhcp', 'bind', 'nfs', 'samba', 'rsync', 'sersync', 'vsftpd', 'subversion'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '数据库中间件',
|
||||||
|
icon: 'Coin',
|
||||||
|
items: ['mycat'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -299,6 +383,7 @@ export function getLanguage(format) {
|
|||||||
cnf: 'ini',
|
cnf: 'ini',
|
||||||
cfg: 'ini',
|
cfg: 'ini',
|
||||||
ora: 'ini',
|
ora: 'ini',
|
||||||
|
rb: 'ruby',
|
||||||
yml: 'yaml',
|
yml: 'yaml',
|
||||||
yaml: 'yaml',
|
yaml: 'yaml',
|
||||||
json: 'json',
|
json: 'json',
|
||||||
@@ -306,6 +391,7 @@ export function getLanguage(format) {
|
|||||||
properties: 'properties',
|
properties: 'properties',
|
||||||
ini: 'ini',
|
ini: 'ini',
|
||||||
xml: 'xml',
|
xml: 'xml',
|
||||||
|
dockerfile: 'dockerfile',
|
||||||
}
|
}
|
||||||
return map[format] || 'plaintext'
|
return map[format] || 'plaintext'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
export const jenkinsSchema = {
|
||||||
|
id: 'jenkins',
|
||||||
|
name: 'Jenkins',
|
||||||
|
icon: 'Setting',
|
||||||
|
category: 'CI/CD',
|
||||||
|
description: '开源自动化服务器,支持持续集成与持续交付',
|
||||||
|
format: 'yml',
|
||||||
|
fileName: 'jenkins.yaml',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '基础配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'httpPort',
|
||||||
|
label: 'HTTP 端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 8080,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ajpPort',
|
||||||
|
label: 'AJP 端口',
|
||||||
|
type: 'number',
|
||||||
|
default: -1,
|
||||||
|
tip: '-1 表示禁用 AJP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'jenkinsHome',
|
||||||
|
label: 'JENKINS_HOME 目录',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/lib/jenkins',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'numExecutors',
|
||||||
|
label: '执行器数量',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
default: 2,
|
||||||
|
tip: '同时执行的任务数',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mode',
|
||||||
|
label: '节点模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'normal - 正常模式', value: 'normal' },
|
||||||
|
{ label: 'exclusive - 仅构建指定任务', value: 'exclusive' },
|
||||||
|
],
|
||||||
|
default: 'normal',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '安全配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'securityRealm',
|
||||||
|
label: '安全域',
|
||||||
|
type: 'text',
|
||||||
|
default: 'default',
|
||||||
|
tip: '用户认证方式',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enableLdap',
|
||||||
|
label: '启用 LDAP 认证',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ldapServer',
|
||||||
|
label: 'LDAP 服务器',
|
||||||
|
type: 'text',
|
||||||
|
default: 'ldap://ldap.example.com',
|
||||||
|
dependsOn: { key: 'enableLdap', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ldapRootDn',
|
||||||
|
label: 'LDAP Root DN',
|
||||||
|
type: 'text',
|
||||||
|
default: 'dc=example,dc=com',
|
||||||
|
dependsOn: { key: 'enableLdap', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authorizationStrategy',
|
||||||
|
label: '授权策略',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '登录用户可以做任何事', value: 'loggedInUsersCanDoAnything' },
|
||||||
|
{ label: '项目矩阵授权', value: 'projectMatrix' },
|
||||||
|
],
|
||||||
|
default: 'loggedInUsersCanDoAnything',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'adminUser',
|
||||||
|
label: '管理员用户名',
|
||||||
|
type: 'text',
|
||||||
|
default: 'admin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'adminPassword',
|
||||||
|
label: '管理员密码',
|
||||||
|
type: 'text',
|
||||||
|
default: 'admin',
|
||||||
|
tip: '生产环境请修改默认密码',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'JCasC (Configuration as Code)',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableCasc',
|
||||||
|
label: '启用 JCasC',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: 'Configuration as Code 插件',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'jenkinsUrl',
|
||||||
|
label: 'Jenkins URL',
|
||||||
|
type: 'text',
|
||||||
|
default: 'http://localhost:8080/',
|
||||||
|
tip: 'Jenkins 系统 URL',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Agent 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableAgent',
|
||||||
|
label: '启用 Agent 节点',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '允许远程 Agent 连接',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'agentPort',
|
||||||
|
label: 'Agent 端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 50000,
|
||||||
|
dependsOn: { key: 'enableAgent', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Git 与 Gitea 集成',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableGitea',
|
||||||
|
label: '启用 Gitea 集成',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'giteaUrl',
|
||||||
|
label: 'Gitea 服务器 URL',
|
||||||
|
type: 'text',
|
||||||
|
default: 'http://gitea.example.com',
|
||||||
|
dependsOn: { key: 'enableGitea', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enableGit',
|
||||||
|
label: '启用 Git',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gitPath',
|
||||||
|
label: 'Git 可执行文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/usr/bin/git',
|
||||||
|
dependsOn: { key: 'enableGit', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateJenkinsYaml(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# Jenkins Configuration as Code - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`jenkins:`)
|
||||||
|
|
||||||
|
// System
|
||||||
|
lines.push(` systemMessage: "Jenkins configured via ConfTemplate"`)
|
||||||
|
lines.push(` numExecutors: ${config.numExecutors}`)
|
||||||
|
lines.push(` mode: ${config.mode}`)
|
||||||
|
lines.push(` jenkinsHome: "${config.jenkinsHome}"`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Security Realm
|
||||||
|
if (config.enableLdap) {
|
||||||
|
lines.push(` securityRealm:`)
|
||||||
|
lines.push(` ldap:`)
|
||||||
|
lines.push(` configurations:`)
|
||||||
|
lines.push(` - server: "${config.ldapServer}"`)
|
||||||
|
lines.push(` rootDN: "${config.ldapRootDn}"`)
|
||||||
|
} else {
|
||||||
|
lines.push(` securityRealm:`)
|
||||||
|
lines.push(` local:`)
|
||||||
|
lines.push(` allowsSignup: false`)
|
||||||
|
lines.push(` users:`)
|
||||||
|
lines.push(` - id: "${config.adminUser}"`)
|
||||||
|
lines.push(` password: "${config.adminPassword}"`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
if (config.authorizationStrategy === 'loggedInUsersCanDoAnything') {
|
||||||
|
lines.push(` authorizationStrategy:`)
|
||||||
|
lines.push(` loggedInUsersCanDoAnything:`)
|
||||||
|
lines.push(` allowAnonymousRead: false`)
|
||||||
|
} else {
|
||||||
|
lines.push(` authorizationStrategy:`)
|
||||||
|
lines.push(` projectMatrix:`)
|
||||||
|
lines.push(` entries:`)
|
||||||
|
lines.push(` - user:"${config.adminUser}":`)
|
||||||
|
lines.push(` permissions:`)
|
||||||
|
lines.push(` - "Overall/Administer"`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Agent
|
||||||
|
if (config.enableAgent) {
|
||||||
|
lines.push(` agent:`)
|
||||||
|
lines.push(` port: ${config.agentPort}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JCasC
|
||||||
|
if (config.enableCasc) {
|
||||||
|
lines.push(` configuration-as-code:`)
|
||||||
|
lines.push(` enabled: true`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// URL
|
||||||
|
lines.push(`unclassified:`)
|
||||||
|
lines.push(` jenkinsLocationConfiguration:`)
|
||||||
|
lines.push(` jenkinsUrl: "${config.jenkinsUrl}"`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Git
|
||||||
|
if (config.enableGit) {
|
||||||
|
lines.push(` git:`)
|
||||||
|
lines.push(` installations:`)
|
||||||
|
lines.push(` - name: "Default"`)
|
||||||
|
lines.push(` home: "${config.gitPath}"`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gitea
|
||||||
|
if (config.enableGitea) {
|
||||||
|
lines.push(` giteaServers:`)
|
||||||
|
lines.push(` servers:`)
|
||||||
|
lines.push(` - displayName: "Gitea"`)
|
||||||
|
lines.push(` serverUrl: "${config.giteaUrl}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
+252
-25
@@ -1,9 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* K8s CronJob 生产级 Schema — 由 ConfTemplate 生成
|
||||||
|
* 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount
|
||||||
|
*/
|
||||||
|
|
||||||
export const k8sCronJobSchema = {
|
export const k8sCronJobSchema = {
|
||||||
id: 'k8s-cronjob',
|
id: 'k8s-cronjob',
|
||||||
name: 'K8s CronJob',
|
name: 'K8s CronJob',
|
||||||
icon: 'Box',
|
icon: 'Box',
|
||||||
category: 'K8S 工作负载',
|
category: 'K8S 工作负载',
|
||||||
description: 'Kubernetes CronJob — 定时调度批处理任务',
|
description: 'Kubernetes CronJob — 定时调度批处理任务(生产级)',
|
||||||
format: 'yaml',
|
format: 'yaml',
|
||||||
fileName: 'cronjob.yaml',
|
fileName: 'cronjob.yaml',
|
||||||
groups: [
|
groups: [
|
||||||
@@ -33,6 +38,17 @@ export const k8sCronJobSchema = {
|
|||||||
default: 'busybox:latest',
|
default: 'busybox:latest',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'imagePullPolicy',
|
||||||
|
label: '镜像拉取策略',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
||||||
|
{ label: 'Always (每次拉取)', value: 'Always' },
|
||||||
|
{ label: 'Never (仅本地)', value: 'Never' },
|
||||||
|
],
|
||||||
|
default: 'IfNotPresent',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'command',
|
key: 'command',
|
||||||
label: 'Command',
|
label: 'Command',
|
||||||
@@ -118,52 +134,264 @@ export const k8sCronJobSchema = {
|
|||||||
max: 100,
|
max: 100,
|
||||||
default: 6,
|
default: 6,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'activeDeadlineSeconds',
|
||||||
|
label: '超时时间 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 86400,
|
||||||
|
default: 0,
|
||||||
|
tip: '0 表示不限制超时',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ttlSecondsAfterFinished',
|
||||||
|
label: '自动清理 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 86400,
|
||||||
|
default: 0,
|
||||||
|
tip: '完成后自动删除 Job,0 表示保留',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '容器端口',
|
||||||
|
fields: [
|
||||||
|
{ key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
|
||||||
|
{ key: 'portName', label: '端口名称', type: 'text', default: 'http' },
|
||||||
|
{ key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '资源限制',
|
||||||
|
fields: [
|
||||||
|
{ key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
|
||||||
|
{ label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
|
||||||
|
{ label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
|
||||||
|
], default: '100m' },
|
||||||
|
{ key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
|
||||||
|
{ label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
|
||||||
|
{ label: '2', value: '2' },{ label: '4', value: '4' },
|
||||||
|
], default: '500m' },
|
||||||
|
{ key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
|
||||||
|
{ label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
|
||||||
|
{ label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
|
||||||
|
], default: '128Mi' },
|
||||||
|
{ key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
|
||||||
|
{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
|
||||||
|
{ label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },{ label: '4Gi', value: '4Gi' },
|
||||||
|
], default: '256Mi' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '环境变量',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false },
|
||||||
|
{ key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'APP_ENV=production\nLOG_LEVEL=info', default: 'APP_ENV=production\nLOG_LEVEL=info', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' },
|
||||||
|
{ key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false },
|
||||||
|
{ key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } },
|
||||||
|
{ key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
|
||||||
|
{ key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '卷挂载',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false },
|
||||||
|
{ key: 'volumeType', label: '卷类型', type: 'select', options: [
|
||||||
|
{ label: 'emptyDir - 临时目录', value: 'emptyDir' },
|
||||||
|
{ label: 'PersistentVolumeClaim', value: 'pvc' },
|
||||||
|
{ label: 'ConfigMap', value: 'configMap' },
|
||||||
|
{ label: 'Secret', value: 'secret' },
|
||||||
|
{ label: 'HostPath - 主机路径', value: 'hostPath' },
|
||||||
|
], default: 'emptyDir', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeName', label: '卷名称', type: 'text', default: 'data', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeMountPath', label: '挂载路径', type: 'text', default: '/data', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeReadOnly', label: '只读挂载', type: 'switch', default: false, dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'pvcClaimName', label: 'PVC 名称', type: 'text', default: 'my-pvc', dependsOn: { key: 'volumeType', value: 'pvc' } },
|
||||||
|
{ key: 'configMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'volumeType', value: 'configMap' } },
|
||||||
|
{ key: 'secretVolumeName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'volumeType', value: 'secret' } },
|
||||||
|
{ key: 'hostPath', label: '主机路径', type: 'text', default: '/tmp/data', dependsOn: { key: 'volumeType', value: 'hostPath' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '安全上下文 (Security Context)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false },
|
||||||
|
{ key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' },
|
||||||
|
{ key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'ServiceAccount',
|
||||||
|
fields: [
|
||||||
|
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
function toYaml(obj, indent = 0) {
|
// ======================== YAML 生成器 ========================
|
||||||
const lines = []
|
|
||||||
const prefix = ' '.repeat(indent)
|
|
||||||
|
|
||||||
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 (value === null || value === undefined || value === '') continue
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(prefix + key + ':')
|
||||||
for (const item of value) {
|
for (var ai = 0; ai < value.length; ai++) {
|
||||||
|
var item = value[ai]
|
||||||
if (typeof item === 'object' && item !== null) {
|
if (typeof item === 'object' && item !== null) {
|
||||||
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
lines.push(renderArrayObject(item, indent + 1))
|
||||||
if (entries.length === 0) continue
|
|
||||||
const first = entries[0]
|
|
||||||
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
|
|
||||||
for (let i = 1; i < entries.length; i++) {
|
|
||||||
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
lines.push(`${prefix} - ${item}`)
|
lines.push(prefix + ' - ' + item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(prefix + key + ':')
|
||||||
lines.push(toYaml(value, indent + 1))
|
lines.push(toYaml(value, indent + 1))
|
||||||
} else {
|
} 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')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateK8sCronJobYaml(config) {
|
export function generateK8sCronJobYaml(config) {
|
||||||
const container = {
|
var container = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
image: config.image,
|
image: config.image,
|
||||||
|
imagePullPolicy: config.imagePullPolicy,
|
||||||
command: config.command.split(' '),
|
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',
|
apiVersion: 'batch/v1',
|
||||||
kind: 'CronJob',
|
kind: 'CronJob',
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -179,18 +407,17 @@ export function generateK8sCronJobYaml(config) {
|
|||||||
jobTemplate: {
|
jobTemplate: {
|
||||||
spec: {
|
spec: {
|
||||||
backoffLimit: config.backoffLimit,
|
backoffLimit: config.backoffLimit,
|
||||||
|
activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined,
|
||||||
|
ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined,
|
||||||
template: {
|
template: {
|
||||||
spec: {
|
spec: podSpec,
|
||||||
containers: [container],
|
|
||||||
restartPolicy: config.restartPolicy,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
return header + '\n' + toYaml(cronjob)
|
||||||
}
|
}
|
||||||
|
|||||||
+338
-188
@@ -10,161 +10,358 @@ export const k8sDaemonSetSchema = {
|
|||||||
{
|
{
|
||||||
title: '基础信息',
|
title: '基础信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'name', label: 'DaemonSet 名称', type: 'text', placeholder: 'my-daemonset', default: 'my-daemonset', required: true },
|
||||||
key: 'name',
|
{ key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' },
|
||||||
label: 'DaemonSet 名称',
|
{ key: 'appName', label: '应用名称', type: 'text', placeholder: 'my-app', default: 'my-app', required: true },
|
||||||
type: 'text',
|
{ key: 'image', label: '容器镜像', type: 'text', placeholder: 'fluentd:latest', default: 'fluentd:latest', required: true },
|
||||||
placeholder: 'my-daemonset',
|
{ key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
|
||||||
default: 'my-daemonset',
|
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
||||||
required: true,
|
{ label: 'Always (每次拉取)', value: 'Always' },
|
||||||
},
|
{ label: 'Never (仅本地)', value: 'Never' },
|
||||||
{
|
], default: 'IfNotPresent' },
|
||||||
key: 'namespace',
|
],
|
||||||
label: '命名空间',
|
},
|
||||||
type: 'text',
|
{
|
||||||
placeholder: 'default',
|
title: '容器端口',
|
||||||
default: 'default',
|
fields: [
|
||||||
},
|
{ key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 8080 },
|
||||||
{
|
{ key: 'portName', label: '端口名称', type: 'text', default: 'http' },
|
||||||
key: 'appName',
|
{ key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
|
||||||
label: '应用名称',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'my-app',
|
|
||||||
default: 'my-app',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'image',
|
|
||||||
label: '容器镜像',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'fluentd:latest',
|
|
||||||
default: 'fluentd:latest',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'containerPort',
|
|
||||||
label: '容器端口',
|
|
||||||
type: 'number',
|
|
||||||
min: 1,
|
|
||||||
max: 65535,
|
|
||||||
default: 8080,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '资源限制',
|
title: '资源限制',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
|
||||||
key: 'cpuRequest',
|
{ label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
|
||||||
label: 'CPU Requests',
|
{ label: '500m', value: '500m' },{ label: '1', value: '1' },
|
||||||
type: 'select',
|
], default: '100m' },
|
||||||
options: [
|
{ key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
|
||||||
{ label: '50m', value: '50m' },
|
{ label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
|
||||||
{ label: '100m', value: '100m' },
|
{ label: '2', value: '2' },
|
||||||
{ label: '200m', value: '200m' },
|
], default: '500m' },
|
||||||
{ label: '500m', value: '500m' },
|
{ key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
|
||||||
{ label: '1', value: '1' },
|
{ label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
|
||||||
],
|
{ label: '512Mi', value: '512Mi' },
|
||||||
default: '100m',
|
], default: '128Mi' },
|
||||||
},
|
{ key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
|
||||||
{
|
{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
|
||||||
key: 'cpuLimit',
|
{ label: '1Gi', value: '1Gi' },
|
||||||
label: 'CPU Limits',
|
], default: '256Mi' },
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '200m', value: '200m' },
|
|
||||||
{ label: '500m', value: '500m' },
|
|
||||||
{ label: '1', value: '1' },
|
|
||||||
{ label: '2', value: '2' },
|
|
||||||
],
|
|
||||||
default: '500m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryRequest',
|
|
||||||
label: 'Memory Requests',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '64Mi', value: '64Mi' },
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
],
|
|
||||||
default: '128Mi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryLimit',
|
|
||||||
label: 'Memory Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
],
|
|
||||||
default: '256Mi',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '节点调度',
|
title: '节点调度',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'hostNetwork', label: '使用主机网络', type: 'switch', default: false, tip: 'Pod 将使用宿主机网络命名空间' },
|
||||||
key: 'hostNetwork',
|
{ key: 'tolerationsEnabled', label: '添加 Toleration', type: 'switch', default: false, tip: '允许在有污点的节点上运行' },
|
||||||
label: '使用主机网络',
|
{ key: 'tolerationKey', label: 'Toleration Key', type: 'text', placeholder: 'node-role.kubernetes.io/master', default: '', dependsOn: { key: 'tolerationsEnabled', value: true } },
|
||||||
type: 'switch',
|
{ key: 'tolerationValue', label: 'Toleration Value', type: 'text', placeholder: '', default: '', dependsOn: { key: 'tolerationsEnabled', value: true } },
|
||||||
default: false,
|
|
||||||
tip: 'Pod 将使用宿主机网络命名空间',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tolerationsEnabled',
|
|
||||||
label: '添加 Toleration',
|
|
||||||
type: 'switch',
|
|
||||||
default: false,
|
|
||||||
tip: '允许在有污点的节点上运行',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tolerationKey',
|
|
||||||
label: 'Toleration Key',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'node-role.kubernetes.io/master',
|
|
||||||
default: '',
|
|
||||||
dependsOn: { key: 'tolerationsEnabled', value: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'tolerationValue',
|
|
||||||
label: 'Toleration Value',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '',
|
|
||||||
default: '',
|
|
||||||
dependsOn: { key: 'tolerationsEnabled', value: true },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '更新策略',
|
title: '更新策略',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'updateStrategy', label: '更新策略', type: 'select', options: [
|
||||||
key: 'updateStrategy',
|
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
|
||||||
label: '更新策略',
|
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
|
||||||
type: 'select',
|
], default: 'RollingUpdate' },
|
||||||
options: [
|
],
|
||||||
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
|
},
|
||||||
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
|
{
|
||||||
],
|
title: '启动探针 (Startup Probe)',
|
||||||
default: 'RollingUpdate',
|
fields: [
|
||||||
},
|
{ key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
|
||||||
|
{ key: 'startupProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
{ key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'startupPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 8080, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
{ key: 'startupCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'startupProbeType', value: 'exec' } },
|
||||||
|
{ key: 'startupInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 0, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
{ key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
{ key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
|
||||||
|
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '存活探针 (Liveness Probe)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
|
||||||
|
{ key: 'livenessProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 8080, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
|
||||||
|
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
|
||||||
|
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 20, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '就绪探针 (Readiness Probe)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableReadinessProbe', label: '启用就绪探针', type: 'switch', default: true, tip: '失败则从 Service 端点摘除' },
|
||||||
|
{ key: 'readinessProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 8080, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
|
||||||
|
{ key: 'readinessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'readinessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 5, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '环境变量',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false },
|
||||||
|
{ key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'APP_ENV=production\nLOG_LEVEL=info', default: 'APP_ENV=production\nLOG_LEVEL=info', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' },
|
||||||
|
{ key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false },
|
||||||
|
{ key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } },
|
||||||
|
{ key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
|
||||||
|
{ key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '卷挂载',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false },
|
||||||
|
{ key: 'volumeType', label: '卷类型', type: 'select', options: [
|
||||||
|
{ label: 'emptyDir - 临时目录', value: 'emptyDir' },
|
||||||
|
{ label: 'hostPath - 主机路径', value: 'hostPath' },
|
||||||
|
{ label: 'ConfigMap', value: 'configMap' },
|
||||||
|
{ label: 'Secret', value: 'secret' },
|
||||||
|
], default: 'emptyDir', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeName', label: '卷名称', type: 'text', default: 'data', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeMountPath', label: '挂载路径', type: 'text', default: '/data', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeReadOnly', label: '只读挂载', type: 'switch', default: false, dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'configMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'volumeType', value: 'configMap' } },
|
||||||
|
{ key: 'secretVolumeName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'volumeType', value: 'secret' } },
|
||||||
|
{ key: 'hostPath', label: '主机路径', type: 'text', default: '/tmp/data', dependsOn: { key: 'volumeType', value: 'hostPath' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '节点调度 — 节点亲和性 (Node Affinity)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableNodeAffinity', label: '启用节点亲和性', type: 'switch', default: false, tip: '比 NodeSelector 更灵活的调度规则' },
|
||||||
|
{ key: 'nodeAffinityType', label: '亲和类型', type: 'select', options: [
|
||||||
|
{ label: 'required - 必须满足 (硬性)', value: 'required' },
|
||||||
|
{ label: 'preferred - 尽量满足 (软性)', value: 'preferred' },
|
||||||
|
], default: 'required', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityKey1', label: '匹配标签 Key', type: 'text', placeholder: 'topology.kubernetes.io/zone', default: 'topology.kubernetes.io/zone', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityOperator1', label: '操作符', type: 'select', options: [
|
||||||
|
{ label: 'In - 值在列表中', value: 'In' },
|
||||||
|
{ label: 'NotIn - 值不在列表中', value: 'NotIn' },
|
||||||
|
{ label: 'Exists - 标签存在', value: 'Exists' },
|
||||||
|
{ label: 'DoesNotExist - 标签不存在', value: 'DoesNotExist' },
|
||||||
|
{ label: 'Gt - 大于', value: 'Gt' },
|
||||||
|
{ label: 'Lt - 小于', value: 'Lt' },
|
||||||
|
], default: 'In', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityValues1', label: '匹配值 (逗号分隔)', type: 'text', placeholder: 'cn-east-1a,cn-east-1b', default: 'cn-east-1a,cn-east-1b', dependsOn: { key: 'nodeAffinityOperator1', valueNotIn: ['Exists', 'DoesNotExist'] } },
|
||||||
|
{ key: 'nodeAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 80, dependsOn: { key: 'nodeAffinityType', value: 'preferred' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '安全上下文 (Security Context)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false },
|
||||||
|
{ key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' },
|
||||||
|
{ key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标签与注解',
|
||||||
|
fields: [
|
||||||
|
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用应用名称' },
|
||||||
|
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
|
||||||
|
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
|
||||||
|
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== YAML 生成器 ========================
|
||||||
|
|
||||||
|
function buildProbe(config, prefix, type, path, port, cmd, headers, initDelay, period, timeout, failure, success) {
|
||||||
|
if (!config[prefix]) return null
|
||||||
|
const probeType = config[type] || 'httpGet'
|
||||||
|
const probe = {}
|
||||||
|
|
||||||
|
if (probeType === 'httpGet') {
|
||||||
|
probe.httpGet = { path: config[path], port: config[port], scheme: 'HTTP' }
|
||||||
|
const h = config[headers]
|
||||||
|
if (h) {
|
||||||
|
const hdrs = h.split('\n').filter(Boolean).map(line => {
|
||||||
|
const [k, ...v] = line.split(':')
|
||||||
|
return { name: k.trim(), value: v.join(':').trim() }
|
||||||
|
})
|
||||||
|
if (hdrs.length) probe.httpGet.httpHeaders = hdrs
|
||||||
|
}
|
||||||
|
} else if (probeType === 'tcpSocket') {
|
||||||
|
probe.tcpSocket = { port: config[port] }
|
||||||
|
} else {
|
||||||
|
const c = config[cmd] || 'cat /tmp/healthy'
|
||||||
|
probe.exec = { command: c.split(' ') }
|
||||||
|
}
|
||||||
|
|
||||||
|
probe.initialDelaySeconds = config[initDelay]
|
||||||
|
probe.periodSeconds = config[period]
|
||||||
|
probe.timeoutSeconds = config[timeout]
|
||||||
|
probe.failureThreshold = config[failure]
|
||||||
|
if (success !== null) probe.successThreshold = config[success]
|
||||||
|
return probe
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNodeAffinity(config) {
|
||||||
|
if (!config.enableNodeAffinity) return undefined
|
||||||
|
const key = config.nodeAffinityKey1
|
||||||
|
const op = config.nodeAffinityOperator1
|
||||||
|
const vals = (config.nodeAffinityValues1 || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
const term = { matchExpressions: [{ key, operator: op }] }
|
||||||
|
if (['In', 'NotIn'].includes(op)) term.matchExpressions[0].values = vals
|
||||||
|
|
||||||
|
const affinity = {}
|
||||||
|
if (config.nodeAffinityType === 'required') {
|
||||||
|
affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [term] } }
|
||||||
|
} else {
|
||||||
|
affinity.nodeAffinity = {
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||||
|
weight: config.nodeAffinityWeight || 80,
|
||||||
|
preference: term,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return affinity
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateK8sDaemonSetYaml(config) {
|
||||||
|
const labels = { app: config.appLabel || config.appName }
|
||||||
|
if (config.versionLabel) labels.version = config.versionLabel
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
name: config.appName,
|
||||||
|
image: config.image,
|
||||||
|
imagePullPolicy: config.imagePullPolicy,
|
||||||
|
ports: [{ name: config.portName, containerPort: config.containerPort, protocol: config.protocol }],
|
||||||
|
resources: {
|
||||||
|
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
|
||||||
|
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 探针
|
||||||
|
container.startupProbe = buildProbe(config, 'enableStartupProbe', 'startupProbeType', 'startupPath', 'startupPort', 'startupCommand', null, 'startupInitialDelay', 'startupPeriod', 'startupTimeout', 'startupFailureThreshold', null)
|
||||||
|
container.livenessProbe = buildProbe(config, 'enableLivenessProbe', 'livenessProbeType', 'livenessPath', 'livenessPort', 'livenessCommand', 'livenessHttpHeaders', 'livenessInitialDelay', 'livenessPeriod', 'livenessTimeout', 'livenessFailureThreshold', 'livenessSuccessThreshold')
|
||||||
|
container.readinessProbe = buildProbe(config, 'enableReadinessProbe', 'readinessProbeType', 'readinessPath', 'readinessPort', 'readinessCommand', 'readinessHttpHeaders', 'readinessInitialDelay', 'readinessPeriod', 'readinessTimeout', 'readinessFailureThreshold', 'readinessSuccessThreshold')
|
||||||
|
if (!container.startupProbe) delete container.startupProbe
|
||||||
|
if (!container.livenessProbe) delete container.livenessProbe
|
||||||
|
if (!container.readinessProbe) delete container.readinessProbe
|
||||||
|
|
||||||
|
// 环境变量
|
||||||
|
if (config.enableEnv && config.envVars) {
|
||||||
|
container.env = config.envVars.split('\n').filter(Boolean).map(line => {
|
||||||
|
const [k, ...v] = line.split('=')
|
||||||
|
return { name: k.trim(), value: v.join('=').trim() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) {
|
||||||
|
container.envFrom = []
|
||||||
|
if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } })
|
||||||
|
if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security Context (container-level)
|
||||||
|
if (config.enableSecurityContext) {
|
||||||
|
container.securityContext = {
|
||||||
|
runAsUser: config.runAsUser,
|
||||||
|
runAsGroup: config.runAsGroup,
|
||||||
|
runAsNonRoot: config.runAsNonRoot,
|
||||||
|
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
|
||||||
|
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卷挂载
|
||||||
|
const volumes = []
|
||||||
|
if (config.enableVolumeMount) {
|
||||||
|
container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }]
|
||||||
|
const vol = { name: config.volumeName }
|
||||||
|
const vt = config.volumeType
|
||||||
|
if (vt === 'emptyDir') vol.emptyDir = {}
|
||||||
|
else if (vt === 'configMap') vol.configMap = { name: config.configMapName }
|
||||||
|
else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName }
|
||||||
|
else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' }
|
||||||
|
volumes.push(vol)
|
||||||
|
}
|
||||||
|
|
||||||
|
const podSpec = { containers: [container] }
|
||||||
|
|
||||||
|
if (config.hostNetwork) podSpec.hostNetwork = true
|
||||||
|
|
||||||
|
// ServiceAccount
|
||||||
|
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
|
||||||
|
|
||||||
|
// 优雅终止
|
||||||
|
if (config.terminationGracePeriod) podSpec.terminationGracePeriodSeconds = config.terminationGracePeriod
|
||||||
|
|
||||||
|
// Tolerations (existing simple + new advanced)
|
||||||
|
if (config.tolerationsEnabled && config.tolerationKey) {
|
||||||
|
const toleration = { key: config.tolerationKey, operator: 'Exists', effect: 'NoSchedule' }
|
||||||
|
if (config.tolerationValue) {
|
||||||
|
toleration.operator = 'Equal'
|
||||||
|
toleration.value = config.tolerationValue
|
||||||
|
}
|
||||||
|
podSpec.tolerations = [toleration]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node Affinity
|
||||||
|
const affinity = buildNodeAffinity(config)
|
||||||
|
if (affinity) podSpec.affinity = affinity
|
||||||
|
|
||||||
|
// Volumes
|
||||||
|
if (volumes.length) podSpec.volumes = volumes
|
||||||
|
|
||||||
|
// fsGroup
|
||||||
|
if (config.enableSecurityContext && config.fsGroup) {
|
||||||
|
podSpec.securityContext = { fsGroup: config.fsGroup }
|
||||||
|
}
|
||||||
|
|
||||||
|
const daemonset = {
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'DaemonSet',
|
||||||
|
metadata: { name: config.name, namespace: config.namespace, labels },
|
||||||
|
spec: {
|
||||||
|
selector: { matchLabels: { app: config.appLabel || config.appName } },
|
||||||
|
updateStrategy: config.updateStrategy === 'RollingUpdate'
|
||||||
|
? { type: 'RollingUpdate', rollingUpdate: { maxUnavailable: 1 } }
|
||||||
|
: { type: 'OnDelete' },
|
||||||
|
template: { metadata: { labels }, spec: podSpec },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = `# K8s DaemonSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'daemonset.yaml'}\n`
|
||||||
|
return header + '\n' + toYaml(daemonset)
|
||||||
|
}
|
||||||
|
|
||||||
function toYaml(obj, indent = 0) {
|
function toYaml(obj, indent = 0) {
|
||||||
const lines = []
|
const lines = []
|
||||||
const prefix = ' '.repeat(indent)
|
const prefix = ' '.repeat(indent)
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (value === null || value === undefined || value === '') continue
|
if (value === null || value === undefined || value === '') continue
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(`${prefix}${key}:`)
|
||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
@@ -172,9 +369,18 @@ function toYaml(obj, indent = 0) {
|
|||||||
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||||
if (entries.length === 0) continue
|
if (entries.length === 0) continue
|
||||||
const first = entries[0]
|
const first = entries[0]
|
||||||
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
|
lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`)
|
||||||
|
if (typeof first[1] === 'object') {
|
||||||
|
lines.push(toYaml(first[1], indent + 3))
|
||||||
|
}
|
||||||
for (let i = 1; i < entries.length; i++) {
|
for (let i = 1; i < entries.length; i++) {
|
||||||
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
|
const [k, v] = entries[i]
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
lines.push(`${prefix} ${k}:`)
|
||||||
|
lines.push(toYaml(v, indent + 4))
|
||||||
|
} else {
|
||||||
|
lines.push(`${prefix} ${k}: ${v}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(`${prefix} - ${item}`)
|
lines.push(`${prefix} - ${item}`)
|
||||||
@@ -187,61 +393,5 @@ function toYaml(obj, indent = 0) {
|
|||||||
lines.push(`${prefix}${key}: ${value}`)
|
lines.push(`${prefix}${key}: ${value}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateK8sDaemonSetYaml(config) {
|
|
||||||
const labels = { app: config.appName }
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
name: config.appName,
|
|
||||||
image: config.image,
|
|
||||||
ports: [{ containerPort: config.containerPort, name: 'http' }],
|
|
||||||
resources: {
|
|
||||||
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
|
|
||||||
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const podSpec = {
|
|
||||||
containers: [container],
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.hostNetwork) {
|
|
||||||
podSpec.hostNetwork = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.tolerationsEnabled && config.tolerationKey) {
|
|
||||||
const toleration = { key: config.tolerationKey, operator: 'Exists', effect: 'NoSchedule' }
|
|
||||||
if (config.tolerationValue) {
|
|
||||||
toleration.operator = 'Equal'
|
|
||||||
toleration.value = config.tolerationValue
|
|
||||||
}
|
|
||||||
podSpec.tolerations = [toleration]
|
|
||||||
}
|
|
||||||
|
|
||||||
const daemonset = {
|
|
||||||
apiVersion: 'apps/v1',
|
|
||||||
kind: 'DaemonSet',
|
|
||||||
metadata: {
|
|
||||||
name: config.name,
|
|
||||||
namespace: config.namespace,
|
|
||||||
labels,
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
selector: { matchLabels: { app: config.appName } },
|
|
||||||
updateStrategy: config.updateStrategy === 'RollingUpdate'
|
|
||||||
? { type: 'RollingUpdate', rollingUpdate: { maxUnavailable: 1 } }
|
|
||||||
: { type: 'OnDelete' },
|
|
||||||
template: {
|
|
||||||
metadata: { labels },
|
|
||||||
spec: podSpec,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = `# K8s DaemonSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'daemonset.yaml'}\n`
|
|
||||||
|
|
||||||
return header + '\n' + toYaml(daemonset)
|
|
||||||
}
|
|
||||||
|
|||||||
+406
-349
@@ -2,7 +2,7 @@ export const k8sDeploymentSchema = {
|
|||||||
id: 'k8s-deployment',
|
id: 'k8s-deployment',
|
||||||
name: 'K8s Deployment',
|
name: 'K8s Deployment',
|
||||||
icon: 'Box',
|
icon: 'Box',
|
||||||
category: '云原生',
|
category: 'K8S 工作负载',
|
||||||
description: 'Kubernetes 无状态应用部署控制器',
|
description: 'Kubernetes 无状态应用部署控制器',
|
||||||
format: 'yaml',
|
format: 'yaml',
|
||||||
fileName: 'deployment.yaml',
|
fileName: 'deployment.yaml',
|
||||||
@@ -10,287 +10,434 @@ export const k8sDeploymentSchema = {
|
|||||||
{
|
{
|
||||||
title: '基础信息',
|
title: '基础信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'appName', label: '应用名称', type: 'text', default: 'my-app', required: true, tip: 'Deployment 和 Label 名称' },
|
||||||
key: 'appName',
|
{ key: 'namespace', label: '命名空间', type: 'text', default: 'default' },
|
||||||
label: '应用名称',
|
{ key: 'image', label: '容器镜像', type: 'text', placeholder: 'nginx:1.24', default: 'nginx:1.24', required: true },
|
||||||
type: 'text',
|
{ key: 'replicas', label: '副本数', type: 'number', min: 1, max: 100, default: 3 },
|
||||||
placeholder: 'my-app',
|
{ key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
|
||||||
default: 'my-app',
|
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
||||||
required: true,
|
{ label: 'Always (每次拉取)', value: 'Always' },
|
||||||
tip: '将用作 Deployment 和 Label 的名称',
|
{ label: 'Never (仅本地)', value: 'Never' },
|
||||||
},
|
], default: 'IfNotPresent' },
|
||||||
{
|
|
||||||
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 副本',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '容器端口',
|
title: '容器端口',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
|
||||||
key: 'containerPort',
|
{ key: 'portName', label: '端口名称', type: 'text', default: 'http' },
|
||||||
label: '容器端口',
|
{ key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
|
||||||
type: 'number',
|
|
||||||
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 匹配时使用',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '资源限制',
|
title: '资源限制',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
|
||||||
key: 'cpuRequest',
|
{ label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
|
||||||
label: 'CPU Requests',
|
{ label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
|
||||||
type: 'select',
|
], default: '100m' },
|
||||||
options: [
|
{ key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
|
||||||
{ label: '50m', value: '50m' },
|
{ label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
|
||||||
{ label: '100m', value: '100m' },
|
{ label: '2', value: '2' },{ label: '4', value: '4' },
|
||||||
{ label: '200m', value: '200m' },
|
], default: '500m' },
|
||||||
{ label: '500m', value: '500m' },
|
{ key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
|
||||||
{ label: '1', value: '1' },
|
{ label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
|
||||||
{ label: '2', value: '2' },
|
{ label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
|
||||||
],
|
], default: '128Mi' },
|
||||||
default: '100m',
|
{ key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
|
||||||
},
|
{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
|
||||||
{
|
{ label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },{ label: '4Gi', value: '4Gi' },
|
||||||
key: 'cpuLimit',
|
], default: '256Mi' },
|
||||||
label: 'CPU Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '200m', value: '200m' },
|
|
||||||
{ label: '500m', value: '500m' },
|
|
||||||
{ label: '1', value: '1' },
|
|
||||||
{ label: '2', value: '2' },
|
|
||||||
{ label: '4', value: '4' },
|
|
||||||
],
|
|
||||||
default: '500m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryRequest',
|
|
||||||
label: 'Memory Requests',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '64Mi', value: '64Mi' },
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
],
|
|
||||||
default: '128Mi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryLimit',
|
|
||||||
label: 'Memory Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
{ label: '2Gi', value: '2Gi' },
|
|
||||||
],
|
|
||||||
default: '256Mi',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '健康检查',
|
title: '启动探针 (Startup Probe)',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
|
||||||
key: 'enableLivenessProbe',
|
{ key: 'startupProbeType', label: '探针方式', type: 'select', options: [
|
||||||
label: '存活探针 (Liveness)',
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
type: 'switch',
|
], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
default: true,
|
{ key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
|
||||||
tip: '检测容器是否存活,失败则重启',
|
{ 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: 'livenessProbeType',
|
{ key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
label: '探针方式',
|
{ key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
|
||||||
type: 'select',
|
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
options: [
|
],
|
||||||
{ label: 'HTTP GET', value: 'httpGet' },
|
},
|
||||||
{ label: 'TCP Socket', value: 'tcpSocket' },
|
{
|
||||||
{ label: 'Exec Command', value: 'exec' },
|
title: '存活探针 (Liveness Probe)',
|
||||||
],
|
fields: [
|
||||||
default: 'httpGet',
|
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
|
||||||
dependsOn: { key: 'enableLivenessProbe', value: true },
|
{ 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',
|
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
|
||||||
label: '健康检查路径',
|
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
type: 'text',
|
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
|
||||||
placeholder: '/healthz',
|
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
|
||||||
default: '/healthz',
|
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
dependsOn: { key: 'livenessProbeType', value: 'httpGet' },
|
{ 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: 'livenessPort',
|
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
|
||||||
label: '探针端口',
|
],
|
||||||
type: 'number',
|
},
|
||||||
min: 1,
|
{
|
||||||
max: 65535,
|
title: '就绪探针 (Readiness Probe)',
|
||||||
default: 80,
|
fields: [
|
||||||
dependsOn: { key: 'enableLivenessProbe', value: true },
|
{ 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' },
|
||||||
key: 'enableReadinessProbe',
|
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
label: '就绪探针 (Readiness)',
|
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
type: 'switch',
|
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
default: true,
|
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
|
||||||
tip: '检测容器是否就绪接收流量',
|
{ 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: 'readinessProbeType',
|
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
label: '探针方式',
|
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
type: 'select',
|
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
options: [
|
],
|
||||||
{ label: 'HTTP GET', value: 'httpGet' },
|
},
|
||||||
{ label: 'TCP Socket', value: 'tcpSocket' },
|
{
|
||||||
{ label: 'Exec Command', value: 'exec' },
|
title: '环境变量',
|
||||||
],
|
fields: [
|
||||||
default: 'httpGet',
|
{ key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false },
|
||||||
dependsOn: { key: 'enableReadinessProbe', value: true },
|
{ 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: 'readinessPath',
|
{ key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
|
||||||
label: '就绪检查路径',
|
{ key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
|
||||||
type: 'text',
|
],
|
||||||
placeholder: '/ready',
|
},
|
||||||
default: '/ready',
|
{
|
||||||
dependsOn: { key: 'readinessProbeType', value: 'httpGet' },
|
title: '卷挂载',
|
||||||
},
|
fields: [
|
||||||
{
|
{ key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false },
|
||||||
key: 'readinessPort',
|
{ key: 'volumeType', label: '卷类型', type: 'select', options: [
|
||||||
label: '探针端口',
|
{ label: 'emptyDir - 临时目录', value: 'emptyDir' },
|
||||||
type: 'number',
|
{ label: 'PersistentVolumeClaim', value: 'pvc' },
|
||||||
min: 1,
|
{ label: 'ConfigMap', value: 'configMap' },
|
||||||
max: 65535,
|
{ label: 'Secret', value: 'secret' },
|
||||||
default: 80,
|
{ label: 'HostPath - 主机路径', value: 'hostPath' },
|
||||||
dependsOn: { key: 'enableReadinessProbe', value: true },
|
], 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: '部署策略',
|
title: '部署策略',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'strategy', label: '更新策略', type: 'select', options: [
|
||||||
key: 'strategy',
|
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
|
||||||
label: '更新策略',
|
{ label: 'Recreate - 先删后建', value: 'Recreate' },
|
||||||
type: 'select',
|
], default: 'RollingUpdate' },
|
||||||
options: [
|
{ key: 'maxSurge', label: '最大超出副本数', type: 'select', options: [
|
||||||
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
|
{ label: '0', value: '0' },{ label: '1', value: '1' },{ label: '25%', value: '25%' },
|
||||||
{ label: 'Recreate - 先删后建', value: 'Recreate' },
|
{ label: '50%', value: '50%' },{ label: '100%', value: '100%' },
|
||||||
],
|
], default: '25%', dependsOn: { key: 'strategy', value: 'RollingUpdate' } },
|
||||||
default: '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: 'maxSurge',
|
{ key: 'revisionHistoryLimit', label: '历史版本保留数', type: 'number', min: 0, max: 50, default: 10, tip: '用于回滚' },
|
||||||
label: '最大超出副本数',
|
{ key: 'progressDeadlineSeconds', label: '部署超时 (秒)', type: 'number', min: 60, max: 3600, default: 600, tip: '超时则标记为失败' },
|
||||||
type: 'select',
|
{ key: 'minReadySeconds', label: '最小就绪等待 (秒)', type: 'number', min: 0, max: 300, default: 0, tip: '容器就绪后多久视为可用' },
|
||||||
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' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '标签与注解',
|
title: '标签与注解',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用应用名称' },
|
||||||
key: 'appLabel',
|
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
|
||||||
label: 'app 标签值',
|
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
|
||||||
type: 'text',
|
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== 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) {
|
function toYaml(obj, indent = 0) {
|
||||||
const lines = []
|
const lines = []
|
||||||
const prefix = ' '.repeat(indent)
|
const prefix = ' '.repeat(indent)
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (value === null || value === undefined || value === '') continue
|
if (value === null || value === undefined || value === '') continue
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(`${prefix}${key}:`)
|
||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
@@ -298,9 +445,18 @@ function toYaml(obj, indent = 0) {
|
|||||||
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||||
if (entries.length === 0) continue
|
if (entries.length === 0) continue
|
||||||
const first = entries[0]
|
const first = entries[0]
|
||||||
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
|
lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`)
|
||||||
|
if (typeof first[1] === 'object') {
|
||||||
|
lines.push(toYaml(first[1], indent + 3))
|
||||||
|
}
|
||||||
for (let i = 1; i < entries.length; i++) {
|
for (let i = 1; i < entries.length; i++) {
|
||||||
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
|
const [k, v] = entries[i]
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
lines.push(`${prefix} ${k}:`)
|
||||||
|
lines.push(toYaml(v, indent + 4))
|
||||||
|
} else {
|
||||||
|
lines.push(`${prefix} ${k}: ${v}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(`${prefix} - ${item}`)
|
lines.push(`${prefix} - ${item}`)
|
||||||
@@ -313,104 +469,5 @@ function toYaml(obj, indent = 0) {
|
|||||||
lines.push(`${prefix}${key}: ${value}`)
|
lines.push(`${prefix}${key}: ${value}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function 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
@@ -1,9 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* K8s Job 生产级 Schema — 由 ConfTemplate 生成
|
||||||
|
* 增强: 容器端口、资源限制、环境变量、卷挂载、安全上下文、ServiceAccount
|
||||||
|
*/
|
||||||
|
|
||||||
export const k8sJobSchema = {
|
export const k8sJobSchema = {
|
||||||
id: 'k8s-job',
|
id: 'k8s-job',
|
||||||
name: 'K8s Job',
|
name: 'K8s Job',
|
||||||
icon: 'Box',
|
icon: 'Box',
|
||||||
category: 'K8S 工作负载',
|
category: 'K8S 工作负载',
|
||||||
description: 'Kubernetes Job — 运行一次性批处理任务',
|
description: 'Kubernetes Job — 运行一次性批处理任务(生产级)',
|
||||||
format: 'yaml',
|
format: 'yaml',
|
||||||
fileName: 'job.yaml',
|
fileName: 'job.yaml',
|
||||||
groups: [
|
groups: [
|
||||||
@@ -33,6 +38,17 @@ export const k8sJobSchema = {
|
|||||||
default: 'busybox:latest',
|
default: 'busybox:latest',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'imagePullPolicy',
|
||||||
|
label: '镜像拉取策略',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
||||||
|
{ label: 'Always (每次拉取)', value: 'Always' },
|
||||||
|
{ label: 'Never (仅本地)', value: 'Never' },
|
||||||
|
],
|
||||||
|
default: 'IfNotPresent',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'command',
|
key: 'command',
|
||||||
label: 'Command',
|
label: 'Command',
|
||||||
@@ -109,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) {
|
// ======================== YAML 生成器 ========================
|
||||||
const lines = []
|
|
||||||
const prefix = ' '.repeat(indent)
|
|
||||||
|
|
||||||
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 (value === null || value === undefined || value === '') continue
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(prefix + key + ':')
|
||||||
for (const item of value) {
|
for (var ai = 0; ai < value.length; ai++) {
|
||||||
|
var item = value[ai]
|
||||||
if (typeof item === 'object' && item !== null) {
|
if (typeof item === 'object' && item !== null) {
|
||||||
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
lines.push(renderArrayObject(item, indent + 1))
|
||||||
if (entries.length === 0) continue
|
|
||||||
const first = entries[0]
|
|
||||||
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
|
|
||||||
for (let i = 1; i < entries.length; i++) {
|
|
||||||
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
lines.push(`${prefix} - ${item}`)
|
lines.push(prefix + ' - ' + item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(prefix + key + ':')
|
||||||
lines.push(toYaml(value, indent + 1))
|
lines.push(toYaml(value, indent + 1))
|
||||||
} else {
|
} 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')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateK8sJobYaml(config) {
|
export function generateK8sJobYaml(config) {
|
||||||
const container = {
|
var container = {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
image: config.image,
|
image: config.image,
|
||||||
|
imagePullPolicy: config.imagePullPolicy,
|
||||||
command: config.command.split(' '),
|
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',
|
apiVersion: 'batch/v1',
|
||||||
kind: 'Job',
|
kind: 'Job',
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -163,24 +373,15 @@ export function generateK8sJobYaml(config) {
|
|||||||
backoffLimit: config.backoffLimit,
|
backoffLimit: config.backoffLimit,
|
||||||
completions: config.completions,
|
completions: config.completions,
|
||||||
parallelism: config.parallelism,
|
parallelism: config.parallelism,
|
||||||
|
activeDeadlineSeconds: config.activeDeadlineSeconds > 0 ? config.activeDeadlineSeconds : undefined,
|
||||||
|
ttlSecondsAfterFinished: config.ttlSecondsAfterFinished > 0 ? config.ttlSecondsAfterFinished : undefined,
|
||||||
template: {
|
template: {
|
||||||
spec: {
|
spec: podSpec,
|
||||||
containers: [container],
|
|
||||||
restartPolicy: config.restartPolicy,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.activeDeadlineSeconds > 0) {
|
var header = '# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ' + new Date().toLocaleString('zh-CN') + '\n# 部署命令: kubectl apply -f ' + (config.fileName || 'job.yaml') + '\n'
|
||||||
job.spec.activeDeadlineSeconds = config.activeDeadlineSeconds
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.ttlSecondsAfterFinished > 0) {
|
|
||||||
job.spec.ttlSecondsAfterFinished = config.ttlSecondsAfterFinished
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = `# K8s Job - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'job.yaml'}\n`
|
|
||||||
|
|
||||||
return header + '\n' + toYaml(job)
|
return header + '\n' + toYaml(job)
|
||||||
}
|
}
|
||||||
|
|||||||
+411
-230
@@ -10,217 +10,446 @@ export const k8sPodSchema = {
|
|||||||
{
|
{
|
||||||
title: '基础信息',
|
title: '基础信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'name', label: 'Pod 名称', type: 'text', placeholder: 'my-pod', default: 'my-pod', required: true },
|
||||||
key: 'name',
|
{ key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' },
|
||||||
label: 'Pod 名称',
|
{ key: 'containerName', label: '容器名称', type: 'text', placeholder: 'main', default: 'main', required: true },
|
||||||
type: 'text',
|
{ key: 'image', label: '容器镜像', type: 'text', placeholder: 'nginx:1.24', default: 'nginx:1.24', required: true },
|
||||||
placeholder: 'my-pod',
|
{ key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
|
||||||
default: 'my-pod',
|
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
||||||
required: true,
|
{ label: 'Always', value: 'Always' },
|
||||||
},
|
{ label: 'Never', value: 'Never' },
|
||||||
{
|
], default: 'IfNotPresent' },
|
||||||
key: 'namespace',
|
|
||||||
label: '命名空间',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'default',
|
|
||||||
default: 'default',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'containerName',
|
|
||||||
label: '容器名称',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'main',
|
|
||||||
default: 'main',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'image',
|
|
||||||
label: '容器镜像',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'nginx:1.24',
|
|
||||||
default: 'nginx:1.24',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'imagePullPolicy',
|
|
||||||
label: '镜像拉取策略',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
|
||||||
{ label: 'Always', value: 'Always' },
|
|
||||||
{ label: 'Never', value: 'Never' },
|
|
||||||
],
|
|
||||||
default: 'IfNotPresent',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '容器端口',
|
title: '容器端口',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
|
||||||
key: 'containerPort',
|
{ key: 'portName', label: '端口名称', type: 'text', default: 'http' },
|
||||||
label: '容器端口',
|
{ key: 'protocol', label: '协议', type: 'select', options: [
|
||||||
type: 'number',
|
{ label: 'TCP', value: 'TCP' },
|
||||||
min: 1,
|
{ label: 'UDP', value: 'UDP' },
|
||||||
max: 65535,
|
], default: 'TCP' },
|
||||||
default: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'protocol',
|
|
||||||
label: '协议',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: 'TCP', value: 'TCP' },
|
|
||||||
{ label: 'UDP', value: 'UDP' },
|
|
||||||
],
|
|
||||||
default: 'TCP',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '启动命令 (可选)',
|
title: '启动命令 (可选)',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'command', label: 'Command', type: 'text', placeholder: '/bin/sh', default: '', tip: '容器 ENTRYPOINT,留空则使用镜像默认值' },
|
||||||
key: 'command',
|
{ key: 'args', label: 'Args', type: 'text', placeholder: '-c, echo hello', default: '', tip: '逗号分隔多个参数' },
|
||||||
label: 'Command',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '/bin/sh',
|
|
||||||
default: '',
|
|
||||||
tip: '容器 ENTRYPOINT,留空则使用镜像默认值',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'args',
|
|
||||||
label: 'Args',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '-c, echo hello',
|
|
||||||
default: '',
|
|
||||||
tip: '逗号分隔多个参数',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '环境变量 (可选)',
|
title: '环境变量 (可选)',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'envVars', label: '环境变量', type: 'keyvalue', default: [], tip: '键值对,将注入为容器环境变量' },
|
||||||
key: 'envVars',
|
{ key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false },
|
||||||
label: '环境变量',
|
{ key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } },
|
||||||
type: 'keyvalue',
|
{ key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
|
||||||
default: [],
|
{ key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
|
||||||
tip: '键值对,将注入为容器环境变量',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '资源限制',
|
title: '资源限制',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
|
||||||
key: 'cpuRequest',
|
{ label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
|
||||||
label: 'CPU Requests',
|
{ label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
|
||||||
type: 'select',
|
], default: '100m' },
|
||||||
options: [
|
{ key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
|
||||||
{ label: '50m', value: '50m' },
|
{ label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
|
||||||
{ label: '100m', value: '100m' },
|
{ label: '2', value: '2' },{ label: '4', value: '4' },
|
||||||
{ label: '200m', value: '200m' },
|
], default: '500m' },
|
||||||
{ label: '500m', value: '500m' },
|
{ key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
|
||||||
{ label: '1', value: '1' },
|
{ label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
|
||||||
{ label: '2', value: '2' },
|
{ label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
|
||||||
],
|
], default: '128Mi' },
|
||||||
default: '100m',
|
{ key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
|
||||||
},
|
{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
|
||||||
{
|
{ label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },
|
||||||
key: 'cpuLimit',
|
], default: '256Mi' },
|
||||||
label: 'CPU Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '200m', value: '200m' },
|
|
||||||
{ label: '500m', value: '500m' },
|
|
||||||
{ label: '1', value: '1' },
|
|
||||||
{ label: '2', value: '2' },
|
|
||||||
{ label: '4', value: '4' },
|
|
||||||
],
|
|
||||||
default: '500m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryRequest',
|
|
||||||
label: 'Memory Requests',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '64Mi', value: '64Mi' },
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
],
|
|
||||||
default: '128Mi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryLimit',
|
|
||||||
label: 'Memory Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
{ label: '2Gi', value: '2Gi' },
|
|
||||||
],
|
|
||||||
default: '256Mi',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '重启与调度',
|
title: '重启与调度',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'restartPolicy', label: '重启策略', type: 'select', options: [
|
||||||
key: 'restartPolicy',
|
{ label: 'Always (默认)', value: 'Always' },
|
||||||
label: '重启策略',
|
{ label: 'OnFailure', value: 'OnFailure' },
|
||||||
type: 'select',
|
{ label: 'Never', value: 'Never' },
|
||||||
options: [
|
], default: 'Always' },
|
||||||
{ label: 'Always (默认)', value: 'Always' },
|
{ key: 'nodeName', label: '指定节点', type: 'text', placeholder: 'node-1', default: '', tip: '留空则由调度器自动分配' },
|
||||||
{ label: 'OnFailure', value: 'OnFailure' },
|
|
||||||
{ label: 'Never', value: 'Never' },
|
|
||||||
],
|
|
||||||
default: 'Always',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'nodeName',
|
|
||||||
label: '指定节点',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'node-1',
|
|
||||||
default: '',
|
|
||||||
tip: '留空则由调度器自动分配',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '标签',
|
title: '标签',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用 Pod 名称' },
|
||||||
key: 'appLabel',
|
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
|
||||||
label: 'app 标签值',
|
],
|
||||||
type: 'text',
|
},
|
||||||
default: '',
|
{
|
||||||
tip: '留空则使用 Pod 名称',
|
title: '启动探针 (Startup Probe)',
|
||||||
},
|
fields: [
|
||||||
{
|
{ key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
|
||||||
key: 'versionLabel',
|
{ key: 'startupProbeType', label: '探针方式', type: 'select', options: [
|
||||||
label: 'version 标签',
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
type: 'text',
|
], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
placeholder: 'v1',
|
{ key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
|
||||||
default: 'v1',
|
{ key: 'startupPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
},
|
{ key: 'startupCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'startupProbeType', value: 'exec' } },
|
||||||
|
{ key: 'startupInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 0, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
{ key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
{ key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
|
||||||
|
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '存活探针 (Liveness Probe)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
|
||||||
|
{ key: 'livenessProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
|
||||||
|
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
|
||||||
|
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 20, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '就绪探针 (Readiness Probe)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableReadinessProbe', label: '启用就绪探针', type: 'switch', default: true, tip: '失败则从 Service 端点摘除' },
|
||||||
|
{ key: 'readinessProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
|
||||||
|
{ key: 'readinessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'readinessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 5, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '卷挂载',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableVolumeMount', label: '挂载卷', type: 'switch', default: false },
|
||||||
|
{ key: 'volumeType', label: '卷类型', type: 'select', options: [
|
||||||
|
{ label: 'emptyDir - 临时目录', value: 'emptyDir' },
|
||||||
|
{ label: 'PersistentVolumeClaim', value: 'pvc' },
|
||||||
|
{ label: 'ConfigMap', value: 'configMap' },
|
||||||
|
{ label: 'Secret', value: 'secret' },
|
||||||
|
{ label: 'HostPath - 主机路径', value: 'hostPath' },
|
||||||
|
], default: 'emptyDir', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeName', label: '卷名称', type: 'text', default: 'data', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeMountPath', label: '挂载路径', type: 'text', default: '/data', dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'volumeReadOnly', label: '只读挂载', type: 'switch', default: false, dependsOn: { key: 'enableVolumeMount', value: true } },
|
||||||
|
{ key: 'pvcClaimName', label: 'PVC 名称', type: 'text', default: 'my-pvc', dependsOn: { key: 'volumeType', value: 'pvc' } },
|
||||||
|
{ key: 'configMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'volumeType', value: 'configMap' } },
|
||||||
|
{ key: 'secretVolumeName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'volumeType', value: 'secret' } },
|
||||||
|
{ key: 'hostPath', label: '主机路径', type: 'text', default: '/tmp/data', dependsOn: { key: 'volumeType', value: 'hostPath' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '节点调度 — NodeSelector',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableNodeSelector', label: '启用 NodeSelector', type: 'switch', default: false, tip: '简单的节点标签匹配' },
|
||||||
|
{ key: 'nodeSelectorKey', label: '标签 Key', type: 'text', placeholder: 'kubernetes.io/os', default: 'kubernetes.io/os', dependsOn: { key: 'enableNodeSelector', value: true } },
|
||||||
|
{ key: 'nodeSelectorValue', label: '标签 Value', type: 'text', placeholder: 'linux', default: 'linux', dependsOn: { key: 'enableNodeSelector', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '节点调度 — 节点亲和性 (Node Affinity)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableNodeAffinity', label: '启用节点亲和性', type: 'switch', default: false, tip: '比 NodeSelector 更灵活的调度规则' },
|
||||||
|
{ key: 'nodeAffinityType', label: '亲和类型', type: 'select', options: [
|
||||||
|
{ label: 'required - 必须满足 (硬性)', value: 'required' },
|
||||||
|
{ label: 'preferred - 尽量满足 (软性)', value: 'preferred' },
|
||||||
|
], default: 'required', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityKey1', label: '匹配标签 Key', type: 'text', placeholder: 'topology.kubernetes.io/zone', default: 'topology.kubernetes.io/zone', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityOperator1', label: '操作符', type: 'select', options: [
|
||||||
|
{ label: 'In - 值在列表中', value: 'In' },
|
||||||
|
{ label: 'NotIn - 值不在列表中', value: 'NotIn' },
|
||||||
|
{ label: 'Exists - 标签存在', value: 'Exists' },
|
||||||
|
{ label: 'DoesNotExist - 标签不存在', value: 'DoesNotExist' },
|
||||||
|
{ label: 'Gt - 大于', value: 'Gt' },
|
||||||
|
{ label: 'Lt - 小于', value: 'Lt' },
|
||||||
|
], default: 'In', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityValues1', label: '匹配值 (逗号分隔)', type: 'text', placeholder: 'cn-east-1a,cn-east-1b', default: 'cn-east-1a,cn-east-1b', dependsOn: { key: 'nodeAffinityOperator1', valueNotIn: ['Exists', 'DoesNotExist'] } },
|
||||||
|
{ key: 'nodeAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 80, dependsOn: { key: 'nodeAffinityType', value: 'preferred' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Pod 反亲和性 (Pod Anti-Affinity)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enablePodAntiAffinity', label: '启用 Pod 反亲和性', type: 'switch', default: false, tip: '让副本分散到不同节点/区域,提高可用性' },
|
||||||
|
{ key: 'podAntiAffinityType', label: '亲和类型', type: 'select', options: [
|
||||||
|
{ label: 'required - 必须分散 (硬性)', value: 'required' },
|
||||||
|
{ label: 'preferred - 尽量分散 (软性)', value: 'preferred' },
|
||||||
|
], default: 'preferred', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
|
||||||
|
{ key: 'podAntiAffinityKey', label: '匹配标签 Key', type: 'text', default: 'app', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
|
||||||
|
{ key: 'podAntiAffinityTopology', label: '拓扑域', type: 'select', options: [
|
||||||
|
{ label: 'kubernetes.io/hostname (节点级)', value: 'kubernetes.io/hostname' },
|
||||||
|
{ label: 'topology.kubernetes.io/zone (可用区级)', value: 'topology.kubernetes.io/zone' },
|
||||||
|
{ label: 'topology.kubernetes.io/region (地域级)', value: 'topology.kubernetes.io/region' },
|
||||||
|
], default: 'kubernetes.io/hostname', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
|
||||||
|
{ key: 'podAntiAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 100, dependsOn: { key: 'podAntiAffinityType', value: 'preferred' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '容忍度 (Tolerations)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableToleration', label: '启用容忍度', type: 'switch', default: false, tip: '允许调度到有 taint 的节点' },
|
||||||
|
{ key: 'tolerationKey', label: 'Taint Key', type: 'text', placeholder: 'node-role.kubernetes.io/master', default: 'node-role.kubernetes.io/master', dependsOn: { key: 'enableToleration', value: true } },
|
||||||
|
{ key: 'tolerationOperator', label: '操作符', type: 'select', options: [
|
||||||
|
{ label: 'Equal - 值匹配', value: 'Equal' },
|
||||||
|
{ label: 'Exists - Key 存在即可', value: 'Exists' },
|
||||||
|
], default: 'Exists', dependsOn: { key: 'enableToleration', value: true } },
|
||||||
|
{ key: 'tolerationValue', label: 'Taint Value', type: 'text', default: '', dependsOn: { key: 'tolerationOperator', value: 'Equal' } },
|
||||||
|
{ key: 'tolerationEffect', label: 'Taint Effect', type: 'select', options: [
|
||||||
|
{ label: 'NoSchedule - 不调度新 Pod', value: 'NoSchedule' },
|
||||||
|
{ label: 'PreferNoSchedule - 尽量不调度', value: 'PreferNoSchedule' },
|
||||||
|
{ label: 'NoExecute - 驱逐已有 Pod', value: 'NoExecute' },
|
||||||
|
], default: 'NoSchedule', dependsOn: { key: 'enableToleration', value: true } },
|
||||||
|
{ key: 'tolerationSeconds', label: '容忍时长 (秒)', type: 'number', min: 0, max: 86400, default: 300, dependsOn: { key: 'tolerationEffect', value: 'NoExecute' }, tip: '仅 NoExecute 有效' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '安全上下文 (Security Context)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false },
|
||||||
|
{ key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' },
|
||||||
|
{ key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '高级选项',
|
||||||
|
fields: [
|
||||||
|
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
|
||||||
|
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
|
||||||
|
{ key: 'hostNetwork', label: '使用主机网络', type: 'switch', default: false, tip: 'Pod 将使用宿主机网络命名空间' },
|
||||||
|
{ key: 'dnsPolicy', label: 'DNS 策略', type: 'select', options: [
|
||||||
|
{ label: 'ClusterFirst (默认)', value: 'ClusterFirst' },
|
||||||
|
{ label: 'Default - 继承节点 DNS', value: 'Default' },
|
||||||
|
{ label: 'ClusterFirstWithHostNet - hostNetwork 时集群优先', value: 'ClusterFirstWithHostNet' },
|
||||||
|
{ label: 'None - 使用 dnsConfig', value: 'None' },
|
||||||
|
], default: 'ClusterFirst' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== YAML 生成器 ========================
|
||||||
|
|
||||||
|
function buildProbe(config, prefix, type, path, port, cmd, headers, initDelay, period, timeout, failure, success) {
|
||||||
|
if (!config[prefix]) return null
|
||||||
|
const probeType = config[type] || 'httpGet'
|
||||||
|
const probe = {}
|
||||||
|
|
||||||
|
if (probeType === 'httpGet') {
|
||||||
|
probe.httpGet = { path: config[path], port: config[port], scheme: 'HTTP' }
|
||||||
|
const h = config[headers]
|
||||||
|
if (h) {
|
||||||
|
const hdrs = h.split('\n').filter(Boolean).map(line => {
|
||||||
|
const [k, ...v] = line.split(':')
|
||||||
|
return { name: k.trim(), value: v.join(':').trim() }
|
||||||
|
})
|
||||||
|
if (hdrs.length) probe.httpGet.httpHeaders = hdrs
|
||||||
|
}
|
||||||
|
} else if (probeType === 'tcpSocket') {
|
||||||
|
probe.tcpSocket = { port: config[port] }
|
||||||
|
} else {
|
||||||
|
const c = config[cmd] || 'cat /tmp/healthy'
|
||||||
|
probe.exec = { command: c.split(' ') }
|
||||||
|
}
|
||||||
|
|
||||||
|
probe.initialDelaySeconds = config[initDelay]
|
||||||
|
probe.periodSeconds = config[period]
|
||||||
|
probe.timeoutSeconds = config[timeout]
|
||||||
|
probe.failureThreshold = config[failure]
|
||||||
|
if (success !== null) probe.successThreshold = config[success]
|
||||||
|
return probe
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAffinity(config) {
|
||||||
|
const affinity = {}
|
||||||
|
|
||||||
|
// Node Affinity
|
||||||
|
if (config.enableNodeAffinity) {
|
||||||
|
const key = config.nodeAffinityKey1
|
||||||
|
const op = config.nodeAffinityOperator1
|
||||||
|
const vals = (config.nodeAffinityValues1 || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
const term = { matchExpressions: [{ key, operator: op }] }
|
||||||
|
if (['In', 'NotIn'].includes(op)) term.matchExpressions[0].values = vals
|
||||||
|
|
||||||
|
if (config.nodeAffinityType === 'required') {
|
||||||
|
affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [term] } }
|
||||||
|
} else {
|
||||||
|
affinity.nodeAffinity = {
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||||
|
weight: config.nodeAffinityWeight || 80,
|
||||||
|
preference: term,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pod Anti-Affinity
|
||||||
|
if (config.enablePodAntiAffinity) {
|
||||||
|
const labelKey = config.podAntiAffinityKey || 'app'
|
||||||
|
const topology = config.podAntiAffinityTopology || 'kubernetes.io/hostname'
|
||||||
|
const term = {
|
||||||
|
labelSelector: { matchExpressions: [{ key: labelKey, operator: 'In', values: [config.appLabel || config.name] }] },
|
||||||
|
topologyKey: topology,
|
||||||
|
}
|
||||||
|
if (config.podAntiAffinityType === 'required') {
|
||||||
|
affinity.podAntiAffinity = { requiredDuringSchedulingIgnoredDuringExecution: [term] }
|
||||||
|
} else {
|
||||||
|
affinity.podAntiAffinity = {
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||||
|
weight: config.podAntiAffinityWeight || 100,
|
||||||
|
podAffinityTerm: term,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(affinity).length ? affinity : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateK8sPodYaml(config) {
|
||||||
|
const labels = { app: config.appLabel || config.name }
|
||||||
|
if (config.versionLabel) labels.version = config.versionLabel
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
name: config.containerName,
|
||||||
|
image: config.image,
|
||||||
|
imagePullPolicy: config.imagePullPolicy,
|
||||||
|
ports: [{ name: config.portName, containerPort: config.containerPort, protocol: config.protocol }],
|
||||||
|
resources: {
|
||||||
|
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
|
||||||
|
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.command) container.command = config.command.split(',').map(s => s.trim())
|
||||||
|
if (config.args) container.args = config.args.split(',').map(s => s.trim())
|
||||||
|
if (config.envVars && config.envVars.length > 0) {
|
||||||
|
container.env = config.envVars.map(e => ({ name: e.key, value: e.value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// envFrom
|
||||||
|
if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) {
|
||||||
|
container.envFrom = []
|
||||||
|
if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } })
|
||||||
|
if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 探针
|
||||||
|
container.startupProbe = buildProbe(config, 'enableStartupProbe', 'startupProbeType', 'startupPath', 'startupPort', 'startupCommand', null, 'startupInitialDelay', 'startupPeriod', 'startupTimeout', 'startupFailureThreshold', null)
|
||||||
|
container.livenessProbe = buildProbe(config, 'enableLivenessProbe', 'livenessProbeType', 'livenessPath', 'livenessPort', 'livenessCommand', 'livenessHttpHeaders', 'livenessInitialDelay', 'livenessPeriod', 'livenessTimeout', 'livenessFailureThreshold', 'livenessSuccessThreshold')
|
||||||
|
container.readinessProbe = buildProbe(config, 'enableReadinessProbe', 'readinessProbeType', 'readinessPath', 'readinessPort', 'readinessCommand', 'readinessHttpHeaders', 'readinessInitialDelay', 'readinessPeriod', 'readinessTimeout', 'readinessFailureThreshold', 'readinessSuccessThreshold')
|
||||||
|
if (!container.startupProbe) delete container.startupProbe
|
||||||
|
if (!container.livenessProbe) delete container.livenessProbe
|
||||||
|
if (!container.readinessProbe) delete container.readinessProbe
|
||||||
|
|
||||||
|
// Security Context (container-level)
|
||||||
|
if (config.enableSecurityContext) {
|
||||||
|
container.securityContext = {
|
||||||
|
runAsUser: config.runAsUser,
|
||||||
|
runAsGroup: config.runAsGroup,
|
||||||
|
runAsNonRoot: config.runAsNonRoot,
|
||||||
|
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
|
||||||
|
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卷挂载
|
||||||
|
const volumes = []
|
||||||
|
if (config.enableVolumeMount) {
|
||||||
|
container.volumeMounts = [{ name: config.volumeName, mountPath: config.volumeMountPath, readOnly: config.volumeReadOnly || undefined }]
|
||||||
|
const vol = { name: config.volumeName }
|
||||||
|
const vt = config.volumeType
|
||||||
|
if (vt === 'emptyDir') vol.emptyDir = {}
|
||||||
|
else if (vt === 'pvc') vol.persistentVolumeClaim = { claimName: config.pvcClaimName }
|
||||||
|
else if (vt === 'configMap') vol.configMap = { name: config.configMapName }
|
||||||
|
else if (vt === 'secret') vol.secret = { secretName: config.secretVolumeName }
|
||||||
|
else if (vt === 'hostPath') vol.hostPath = { path: config.hostPath, type: 'DirectoryOrCreate' }
|
||||||
|
volumes.push(vol)
|
||||||
|
}
|
||||||
|
|
||||||
|
const podSpec = { containers: [container] }
|
||||||
|
|
||||||
|
podSpec.restartPolicy = config.restartPolicy
|
||||||
|
if (config.nodeName) podSpec.nodeName = config.nodeName
|
||||||
|
|
||||||
|
// ServiceAccount
|
||||||
|
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
|
||||||
|
|
||||||
|
// 优雅终止
|
||||||
|
if (config.terminationGracePeriod) podSpec.terminationGracePeriodSeconds = config.terminationGracePeriod
|
||||||
|
|
||||||
|
// HostNetwork
|
||||||
|
if (config.hostNetwork) podSpec.hostNetwork = true
|
||||||
|
|
||||||
|
// DNS Policy
|
||||||
|
if (config.dnsPolicy && config.dnsPolicy !== 'ClusterFirst') podSpec.dnsPolicy = config.dnsPolicy
|
||||||
|
|
||||||
|
// NodeSelector
|
||||||
|
if (config.enableNodeSelector && config.nodeSelectorKey) {
|
||||||
|
podSpec.nodeSelector = { [config.nodeSelectorKey]: config.nodeSelectorValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affinity
|
||||||
|
const affinity = buildAffinity(config)
|
||||||
|
if (affinity) podSpec.affinity = affinity
|
||||||
|
|
||||||
|
// Tolerations
|
||||||
|
if (config.enableToleration) {
|
||||||
|
const t = { key: config.tolerationKey, operator: config.tolerationOperator, effect: config.tolerationEffect }
|
||||||
|
if (config.tolerationOperator === 'Equal') t.value = config.tolerationValue
|
||||||
|
if (config.tolerationEffect === 'NoExecute' && config.tolerationSeconds) t.tolerationSeconds = config.tolerationSeconds
|
||||||
|
podSpec.tolerations = [t]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volumes
|
||||||
|
if (volumes.length) podSpec.volumes = volumes
|
||||||
|
|
||||||
|
// fsGroup
|
||||||
|
if (config.enableSecurityContext && config.fsGroup) {
|
||||||
|
podSpec.securityContext = { fsGroup: config.fsGroup }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pod = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Pod',
|
||||||
|
metadata: { name: config.name, namespace: config.namespace, labels },
|
||||||
|
spec: podSpec,
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = `# K8s Pod - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'pod.yaml'}\n`
|
||||||
|
return header + '\n' + toYaml(pod)
|
||||||
|
}
|
||||||
|
|
||||||
function toYaml(obj, indent = 0) {
|
function toYaml(obj, indent = 0) {
|
||||||
const lines = []
|
const lines = []
|
||||||
const prefix = ' '.repeat(indent)
|
const prefix = ' '.repeat(indent)
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (value === null || value === undefined || value === '') continue
|
if (value === null || value === undefined || value === '') continue
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
lines.push(`${prefix}${key}:`)
|
lines.push(`${prefix}${key}:`)
|
||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
@@ -228,9 +457,18 @@ function toYaml(obj, indent = 0) {
|
|||||||
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||||
if (entries.length === 0) continue
|
if (entries.length === 0) continue
|
||||||
const first = entries[0]
|
const first = entries[0]
|
||||||
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
|
lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`)
|
||||||
|
if (typeof first[1] === 'object') {
|
||||||
|
lines.push(toYaml(first[1], indent + 3))
|
||||||
|
}
|
||||||
for (let i = 1; i < entries.length; i++) {
|
for (let i = 1; i < entries.length; i++) {
|
||||||
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
|
const [k, v] = entries[i]
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
lines.push(`${prefix} ${k}:`)
|
||||||
|
lines.push(toYaml(v, indent + 4))
|
||||||
|
} else {
|
||||||
|
lines.push(`${prefix} ${k}: ${v}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(`${prefix} - ${item}`)
|
lines.push(`${prefix} - ${item}`)
|
||||||
@@ -243,62 +481,5 @@ function toYaml(obj, indent = 0) {
|
|||||||
lines.push(`${prefix}${key}: ${value}`)
|
lines.push(`${prefix}${key}: ${value}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateK8sPodYaml(config) {
|
|
||||||
const labels = {
|
|
||||||
app: config.appLabel || config.name,
|
|
||||||
}
|
|
||||||
if (config.versionLabel) {
|
|
||||||
labels.version = config.versionLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = {
|
|
||||||
name: config.containerName,
|
|
||||||
image: config.image,
|
|
||||||
imagePullPolicy: config.imagePullPolicy,
|
|
||||||
ports: [{
|
|
||||||
containerPort: config.containerPort,
|
|
||||||
protocol: config.protocol,
|
|
||||||
}],
|
|
||||||
resources: {
|
|
||||||
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
|
|
||||||
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.command) {
|
|
||||||
container.command = config.command.split(',').map(s => s.trim())
|
|
||||||
}
|
|
||||||
if (config.args) {
|
|
||||||
container.args = config.args.split(',').map(s => s.trim())
|
|
||||||
}
|
|
||||||
if (config.envVars && config.envVars.length > 0) {
|
|
||||||
container.env = config.envVars.map(e => ({ name: e.key, value: e.value }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const podSpec = {
|
|
||||||
containers: [container],
|
|
||||||
restartPolicy: config.restartPolicy,
|
|
||||||
}
|
|
||||||
if (config.nodeName) {
|
|
||||||
podSpec.nodeName = config.nodeName
|
|
||||||
}
|
|
||||||
|
|
||||||
const pod = {
|
|
||||||
apiVersion: 'v1',
|
|
||||||
kind: 'Pod',
|
|
||||||
metadata: {
|
|
||||||
name: config.name,
|
|
||||||
namespace: config.namespace,
|
|
||||||
labels,
|
|
||||||
},
|
|
||||||
spec: podSpec,
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = `# K8s Pod - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'pod.yaml'}\n`
|
|
||||||
|
|
||||||
return header + '\n' + toYaml(pod)
|
|
||||||
}
|
|
||||||
|
|||||||
+370
-203
@@ -10,242 +10,348 @@ export const k8sStatefulSetSchema = {
|
|||||||
{
|
{
|
||||||
title: '基础信息',
|
title: '基础信息',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'name', label: 'StatefulSet 名称', type: 'text', placeholder: 'my-statefulset', default: 'my-statefulset', required: true },
|
||||||
key: 'name',
|
{ key: 'namespace', label: '命名空间', type: 'text', placeholder: 'default', default: 'default' },
|
||||||
label: 'StatefulSet 名称',
|
{ key: 'appName', label: '应用名称', type: 'text', placeholder: 'my-app', default: 'my-app', required: true, tip: '用作 selector 和 label 的 app 值' },
|
||||||
type: 'text',
|
{ key: 'image', label: '容器镜像', type: 'text', placeholder: 'nginx:1.24', default: 'nginx:1.24', required: true },
|
||||||
placeholder: 'my-statefulset',
|
{ key: 'replicas', label: '副本数', type: 'number', min: 1, max: 100, default: 3 },
|
||||||
default: 'my-statefulset',
|
{ key: 'imagePullPolicy', label: '镜像拉取策略', type: 'select', options: [
|
||||||
required: true,
|
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
|
||||||
},
|
{ label: 'Always (每次拉取)', value: 'Always' },
|
||||||
{
|
{ label: 'Never (仅本地)', value: 'Never' },
|
||||||
key: 'namespace',
|
], default: 'IfNotPresent' },
|
||||||
label: '命名空间',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'default',
|
|
||||||
default: 'default',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'appName',
|
|
||||||
label: '应用名称',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'my-app',
|
|
||||||
default: 'my-app',
|
|
||||||
required: true,
|
|
||||||
tip: '用作 selector 和 label 的 app 值',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'image',
|
|
||||||
label: '容器镜像',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'nginx:1.24',
|
|
||||||
default: 'nginx:1.24',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'replicas',
|
|
||||||
label: '副本数',
|
|
||||||
type: 'number',
|
|
||||||
min: 1,
|
|
||||||
max: 100,
|
|
||||||
default: 3,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '网络与服务',
|
title: '网络与服务',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'containerPort', label: '容器端口', type: 'number', min: 1, max: 65535, default: 80 },
|
||||||
key: 'containerPort',
|
{ key: 'portName', label: '端口名称', type: 'text', default: 'http' },
|
||||||
label: '容器端口',
|
{ key: 'protocol', label: '协议', type: 'select', options: [{ label: 'TCP', value: 'TCP' }, { label: 'UDP', value: 'UDP' }], default: 'TCP' },
|
||||||
type: 'number',
|
{ key: 'serviceName', label: 'Headless Service 名称', type: 'text', placeholder: 'my-service', default: 'my-service', required: true, tip: '必须已存在的 Headless Service' },
|
||||||
min: 1,
|
|
||||||
max: 65535,
|
|
||||||
default: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'serviceName',
|
|
||||||
label: 'Headless Service 名称',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'my-service',
|
|
||||||
default: 'my-service',
|
|
||||||
required: true,
|
|
||||||
tip: '必须已存在的 Headless Service',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '持久存储',
|
title: '持久存储',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'volumeClaimEnabled', label: '启用 PVC', type: 'switch', default: true, tip: '为每个 Pod 创建独立的 PersistentVolumeClaim' },
|
||||||
key: 'volumeClaimEnabled',
|
{ key: 'storageClassName', label: 'StorageClass', type: 'text', placeholder: 'standard', default: 'standard', dependsOn: { key: 'volumeClaimEnabled', value: true } },
|
||||||
label: '启用 PVC',
|
{ key: 'storageSize', label: '存储大小', type: 'text', placeholder: '10Gi', default: '10Gi', dependsOn: { key: 'volumeClaimEnabled', value: true } },
|
||||||
type: 'switch',
|
{ key: 'pvcAccessMode', label: '访问模式', type: 'select', options: [
|
||||||
default: true,
|
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
||||||
tip: '为每个 Pod 创建独立的 PersistentVolumeClaim',
|
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
||||||
},
|
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
||||||
{
|
], default: 'ReadWriteOnce', dependsOn: { key: 'volumeClaimEnabled', value: true } },
|
||||||
key: 'storageClassName',
|
|
||||||
label: 'StorageClass',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'standard',
|
|
||||||
default: 'standard',
|
|
||||||
dependsOn: { key: 'volumeClaimEnabled', value: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'storageSize',
|
|
||||||
label: '存储大小',
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '10Gi',
|
|
||||||
default: '10Gi',
|
|
||||||
dependsOn: { key: 'volumeClaimEnabled', value: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pvcAccessMode',
|
|
||||||
label: '访问模式',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
|
||||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
|
||||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
|
||||||
],
|
|
||||||
default: 'ReadWriteOnce',
|
|
||||||
dependsOn: { key: 'volumeClaimEnabled', value: true },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '资源限制',
|
title: '资源限制',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'cpuRequest', label: 'CPU Requests', type: 'select', options: [
|
||||||
key: 'cpuRequest',
|
{ label: '50m', value: '50m' },{ label: '100m', value: '100m' },{ label: '200m', value: '200m' },
|
||||||
label: 'CPU Requests',
|
{ label: '500m', value: '500m' },{ label: '1', value: '1' },{ label: '2', value: '2' },
|
||||||
type: 'select',
|
], default: '100m' },
|
||||||
options: [
|
{ key: 'cpuLimit', label: 'CPU Limits', type: 'select', options: [
|
||||||
{ label: '50m', value: '50m' },
|
{ label: '200m', value: '200m' },{ label: '500m', value: '500m' },{ label: '1', value: '1' },
|
||||||
{ label: '100m', value: '100m' },
|
{ label: '2', value: '2' },{ label: '4', value: '4' },
|
||||||
{ label: '200m', value: '200m' },
|
], default: '500m' },
|
||||||
{ label: '500m', value: '500m' },
|
{ key: 'memoryRequest', label: 'Memory Requests', type: 'select', options: [
|
||||||
{ label: '1', value: '1' },
|
{ label: '64Mi', value: '64Mi' },{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },
|
||||||
{ label: '2', value: '2' },
|
{ label: '512Mi', value: '512Mi' },{ label: '1Gi', value: '1Gi' },
|
||||||
],
|
], default: '128Mi' },
|
||||||
default: '100m',
|
{ key: 'memoryLimit', label: 'Memory Limits', type: 'select', options: [
|
||||||
},
|
{ label: '128Mi', value: '128Mi' },{ label: '256Mi', value: '256Mi' },{ label: '512Mi', value: '512Mi' },
|
||||||
{
|
{ label: '1Gi', value: '1Gi' },{ label: '2Gi', value: '2Gi' },{ label: '4Gi', value: '4Gi' },
|
||||||
key: 'cpuLimit',
|
], default: '256Mi' },
|
||||||
label: 'CPU Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '200m', value: '200m' },
|
|
||||||
{ label: '500m', value: '500m' },
|
|
||||||
{ label: '1', value: '1' },
|
|
||||||
{ label: '2', value: '2' },
|
|
||||||
{ label: '4', value: '4' },
|
|
||||||
],
|
|
||||||
default: '500m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryRequest',
|
|
||||||
label: 'Memory Requests',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '64Mi', value: '64Mi' },
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
],
|
|
||||||
default: '128Mi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'memoryLimit',
|
|
||||||
label: 'Memory Limits',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ label: '128Mi', value: '128Mi' },
|
|
||||||
{ label: '256Mi', value: '256Mi' },
|
|
||||||
{ label: '512Mi', value: '512Mi' },
|
|
||||||
{ label: '1Gi', value: '1Gi' },
|
|
||||||
{ label: '2Gi', value: '2Gi' },
|
|
||||||
],
|
|
||||||
default: '256Mi',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '更新策略',
|
title: '更新策略',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{ key: 'updateStrategy', label: '更新策略', type: 'select', options: [
|
||||||
key: 'updateStrategy',
|
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
|
||||||
label: '更新策略',
|
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
|
||||||
type: 'select',
|
], default: 'RollingUpdate' },
|
||||||
options: [
|
{ key: 'partition', label: 'Partition', type: 'number', min: 0, max: 100, default: 0, tip: '仅更新序号 >= partition 的 Pod', dependsOn: { key: 'updateStrategy', value: 'RollingUpdate' } },
|
||||||
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
|
],
|
||||||
{ label: 'OnDelete - 手动删除触发', value: 'OnDelete' },
|
},
|
||||||
],
|
{
|
||||||
default: 'RollingUpdate',
|
title: '启动探针 (Startup Probe)',
|
||||||
},
|
fields: [
|
||||||
{
|
{ key: 'enableStartupProbe', label: '启用启动探针', type: 'switch', default: false, tip: '慢启动容器必备,通过前 liveness/readiness 不会生效' },
|
||||||
key: 'partition',
|
{ key: 'startupProbeType', label: '探针方式', type: 'select', options: [
|
||||||
label: 'Partition',
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
type: 'number',
|
], default: 'httpGet', dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
min: 0,
|
{ key: 'startupPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'startupProbeType', value: 'httpGet' } },
|
||||||
max: 100,
|
{ key: 'startupPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
default: 0,
|
{ key: 'startupCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'startupProbeType', value: 'exec' } },
|
||||||
tip: '仅更新序号 >= partition 的 Pod',
|
{ key: 'startupInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 0, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
dependsOn: { key: 'updateStrategy', value: 'RollingUpdate' },
|
{ key: 'startupPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
},
|
{ key: 'startupFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 100, default: 30, dependsOn: { key: 'enableStartupProbe', value: true }, tip: '30次×10秒=最多等300秒启动' },
|
||||||
|
{ key: 'startupTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableStartupProbe', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '存活探针 (Liveness Probe)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableLivenessProbe', label: '启用存活探针', type: 'switch', default: true, tip: '失败则重启容器' },
|
||||||
|
{ key: 'livenessProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessPath', label: '检查路径', type: 'text', default: '/healthz', dependsOn: { key: 'livenessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'livenessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/healthy', default: 'cat /tmp/healthy', dependsOn: { key: 'livenessProbeType', value: 'exec' } },
|
||||||
|
{ key: 'livenessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'livenessProbeType', value: 'httpGet' }, tip: '格式: Header-Name: value' },
|
||||||
|
{ key: 'livenessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 15, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 20, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 5, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableLivenessProbe', value: true } },
|
||||||
|
{ key: 'livenessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableLivenessProbe', value: true }, tip: 'liveness 只能为 1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '就绪探针 (Readiness Probe)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableReadinessProbe', label: '启用就绪探针', type: 'switch', default: true, tip: '失败则从 Service 端点摘除' },
|
||||||
|
{ key: 'readinessProbeType', label: '探针方式', type: 'select', options: [
|
||||||
|
{ label: 'HTTP GET', value: 'httpGet' },{ label: 'TCP Socket', value: 'tcpSocket' },{ label: 'Exec Command', value: 'exec' },
|
||||||
|
], default: 'httpGet', dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessPath', label: '检查路径', type: 'text', default: '/ready', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'readinessPort', label: '检查端口', type: 'number', min: 1, max: 65535, default: 80, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessCommand', label: 'Exec 命令', type: 'text', placeholder: 'cat /tmp/ready', default: 'cat /tmp/ready', dependsOn: { key: 'readinessProbeType', value: 'exec' } },
|
||||||
|
{ key: 'readinessHttpHeaders', label: 'HTTP 请求头', type: 'text', placeholder: 'X-Custom-Header: value', default: '', dependsOn: { key: 'readinessProbeType', value: 'httpGet' } },
|
||||||
|
{ key: 'readinessInitialDelay', label: '初始延迟 (秒)', type: 'number', min: 0, max: 600, default: 5, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessPeriod', label: '检查周期 (秒)', type: 'number', min: 1, max: 300, default: 10, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessTimeout', label: '超时时间 (秒)', type: 'number', min: 1, max: 60, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessFailureThreshold', label: '失败阈值', type: 'number', min: 1, max: 30, default: 3, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
{ key: 'readinessSuccessThreshold', label: '成功阈值', type: 'number', min: 1, max: 10, default: 1, dependsOn: { key: 'enableReadinessProbe', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '环境变量',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableEnv', label: '定义环境变量', type: 'switch', default: false },
|
||||||
|
{ key: 'envVars', label: '环境变量 (KEY=VALUE)', type: 'text', placeholder: 'APP_ENV=production\nLOG_LEVEL=info', default: 'APP_ENV=production\nLOG_LEVEL=info', dependsOn: { key: 'enableEnv', value: true }, tip: '每行一个,格式 KEY=VALUE' },
|
||||||
|
{ key: 'enableEnvFromConfigMap', label: '从 ConfigMap 导入', type: 'switch', default: false },
|
||||||
|
{ key: 'envFromConfigMapName', label: 'ConfigMap 名称', type: 'text', default: 'my-config', dependsOn: { key: 'enableEnvFromConfigMap', value: true } },
|
||||||
|
{ key: 'enableEnvFromSecret', label: '从 Secret 导入', type: 'switch', default: false },
|
||||||
|
{ key: 'envFromSecretName', label: 'Secret 名称', type: 'text', default: 'my-secret', dependsOn: { key: 'enableEnvFromSecret', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '节点调度 — NodeSelector',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableNodeSelector', label: '启用 NodeSelector', type: 'switch', default: false, tip: '简单的节点标签匹配' },
|
||||||
|
{ key: 'nodeSelectorKey', label: '标签 Key', type: 'text', placeholder: 'kubernetes.io/os', default: 'kubernetes.io/os', dependsOn: { key: 'enableNodeSelector', value: true } },
|
||||||
|
{ key: 'nodeSelectorValue', label: '标签 Value', type: 'text', placeholder: 'linux', default: 'linux', dependsOn: { key: 'enableNodeSelector', value: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '节点调度 — 节点亲和性 (Node Affinity)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableNodeAffinity', label: '启用节点亲和性', type: 'switch', default: false, tip: '比 NodeSelector 更灵活的调度规则' },
|
||||||
|
{ key: 'nodeAffinityType', label: '亲和类型', type: 'select', options: [
|
||||||
|
{ label: 'required - 必须满足 (硬性)', value: 'required' },
|
||||||
|
{ label: 'preferred - 尽量满足 (软性)', value: 'preferred' },
|
||||||
|
], default: 'required', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityKey1', label: '匹配标签 Key', type: 'text', placeholder: 'topology.kubernetes.io/zone', default: 'topology.kubernetes.io/zone', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityOperator1', label: '操作符', type: 'select', options: [
|
||||||
|
{ label: 'In - 值在列表中', value: 'In' },
|
||||||
|
{ label: 'NotIn - 值不在列表中', value: 'NotIn' },
|
||||||
|
{ label: 'Exists - 标签存在', value: 'Exists' },
|
||||||
|
{ label: 'DoesNotExist - 标签不存在', value: 'DoesNotExist' },
|
||||||
|
{ label: 'Gt - 大于', value: 'Gt' },
|
||||||
|
{ label: 'Lt - 小于', value: 'Lt' },
|
||||||
|
], default: 'In', dependsOn: { key: 'enableNodeAffinity', value: true } },
|
||||||
|
{ key: 'nodeAffinityValues1', label: '匹配值 (逗号分隔)', type: 'text', placeholder: 'cn-east-1a,cn-east-1b', default: 'cn-east-1a,cn-east-1b', dependsOn: { key: 'nodeAffinityOperator1', valueNotIn: ['Exists', 'DoesNotExist'] } },
|
||||||
|
{ key: 'nodeAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 80, dependsOn: { key: 'nodeAffinityType', value: 'preferred' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Pod 反亲和性 (Pod Anti-Affinity)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enablePodAntiAffinity', label: '启用 Pod 反亲和性', type: 'switch', default: false, tip: '让副本分散到不同节点/区域,提高可用性' },
|
||||||
|
{ key: 'podAntiAffinityType', label: '亲和类型', type: 'select', options: [
|
||||||
|
{ label: 'required - 必须分散 (硬性)', value: 'required' },
|
||||||
|
{ label: 'preferred - 尽量分散 (软性)', value: 'preferred' },
|
||||||
|
], default: 'preferred', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
|
||||||
|
{ key: 'podAntiAffinityKey', label: '匹配标签 Key', type: 'text', default: 'app', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
|
||||||
|
{ key: 'podAntiAffinityTopology', label: '拓扑域', type: 'select', options: [
|
||||||
|
{ label: 'kubernetes.io/hostname (节点级)', value: 'kubernetes.io/hostname' },
|
||||||
|
{ label: 'topology.kubernetes.io/zone (可用区级)', value: 'topology.kubernetes.io/zone' },
|
||||||
|
{ label: 'topology.kubernetes.io/region (地域级)', value: 'topology.kubernetes.io/region' },
|
||||||
|
], default: 'kubernetes.io/hostname', dependsOn: { key: 'enablePodAntiAffinity', value: true } },
|
||||||
|
{ key: 'podAntiAffinityWeight', label: '权重 (preferred)', type: 'number', min: 1, max: 100, default: 100, dependsOn: { key: 'podAntiAffinityType', value: 'preferred' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '容忍度 (Tolerations)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableToleration', label: '启用容忍度', type: 'switch', default: false, tip: '允许调度到有 taint 的节点' },
|
||||||
|
{ key: 'tolerationKey', label: 'Taint Key', type: 'text', placeholder: 'node-role.kubernetes.io/master', default: 'node-role.kubernetes.io/master', dependsOn: { key: 'enableToleration', value: true } },
|
||||||
|
{ key: 'tolerationOperator', label: '操作符', type: 'select', options: [
|
||||||
|
{ label: 'Equal - 值匹配', value: 'Equal' },
|
||||||
|
{ label: 'Exists - Key 存在即可', value: 'Exists' },
|
||||||
|
], default: 'Exists', dependsOn: { key: 'enableToleration', value: true } },
|
||||||
|
{ key: 'tolerationValue', label: 'Taint Value', type: 'text', default: '', dependsOn: { key: 'tolerationOperator', value: 'Equal' } },
|
||||||
|
{ key: 'tolerationEffect', label: 'Taint Effect', type: 'select', options: [
|
||||||
|
{ label: 'NoSchedule - 不调度新 Pod', value: 'NoSchedule' },
|
||||||
|
{ label: 'PreferNoSchedule - 尽量不调度', value: 'PreferNoSchedule' },
|
||||||
|
{ label: 'NoExecute - 驱逐已有 Pod', value: 'NoExecute' },
|
||||||
|
], default: 'NoSchedule', dependsOn: { key: 'enableToleration', value: true } },
|
||||||
|
{ key: 'tolerationSeconds', label: '容忍时长 (秒)', type: 'number', min: 0, max: 86400, default: 300, dependsOn: { key: 'tolerationEffect', value: 'NoExecute' }, tip: '仅 NoExecute 有效' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '安全上下文 (Security Context)',
|
||||||
|
fields: [
|
||||||
|
{ key: 'enableSecurityContext', label: '启用安全上下文', type: 'switch', default: false },
|
||||||
|
{ key: 'runAsUser', label: '运行用户 UID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsGroup', label: '运行用户组 GID', type: 'number', min: 0, max: 65535, default: 1000, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'runAsNonRoot', label: '禁止 Root 运行', type: 'switch', default: true, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'readOnlyRootFilesystem', label: '只读根文件系统', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '增强安全性,需要写入的目录用 emptyDir 挂载' },
|
||||||
|
{ key: 'allowPrivilegeEscalation', label: '允许提权', type: 'switch', default: false, dependsOn: { key: 'enableSecurityContext', value: true } },
|
||||||
|
{ key: 'fsGroup', label: '文件系统 GID', type: 'number', min: 0, max: 65535, default: 2000, dependsOn: { key: 'enableSecurityContext', value: true }, tip: '卷文件的所有组' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标签与注解',
|
||||||
|
fields: [
|
||||||
|
{ key: 'appLabel', label: 'app 标签值', type: 'text', default: '', tip: '留空则使用应用名称' },
|
||||||
|
{ key: 'versionLabel', label: 'version 标签', type: 'text', placeholder: 'v1', default: 'v1' },
|
||||||
|
{ key: 'serviceAccountName', label: 'ServiceAccount 名称', type: 'text', placeholder: '留空使用 default', default: '', tip: 'RBAC 权限控制' },
|
||||||
|
{ key: 'terminationGracePeriod', label: '优雅终止时间 (秒)', type: 'number', min: 0, max: 300, default: 30, tip: 'SIGTERM 后等待多久发送 SIGKILL' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
function toYaml(obj, indent = 0) {
|
// ======================== YAML 生成器 ========================
|
||||||
const lines = []
|
|
||||||
const prefix = ' '.repeat(indent)
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
function buildProbe(config, prefix, type, path, port, cmd, headers, initDelay, period, timeout, failure, success) {
|
||||||
if (value === null || value === undefined || value === '') continue
|
if (!config[prefix]) return null
|
||||||
|
const probeType = config[type] || 'httpGet'
|
||||||
|
const probe = {}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (probeType === 'httpGet') {
|
||||||
lines.push(`${prefix}${key}:`)
|
probe.httpGet = { path: config[path], port: config[port], scheme: 'HTTP' }
|
||||||
for (const item of value) {
|
const h = config[headers]
|
||||||
if (typeof item === 'object' && item !== null) {
|
if (h) {
|
||||||
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
const hdrs = h.split('\n').filter(Boolean).map(line => {
|
||||||
if (entries.length === 0) continue
|
const [k, ...v] = line.split(':')
|
||||||
const first = entries[0]
|
return { name: k.trim(), value: v.join(':').trim() }
|
||||||
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
|
})
|
||||||
for (let i = 1; i < entries.length; i++) {
|
if (hdrs.length) probe.httpGet.httpHeaders = hdrs
|
||||||
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
|
}
|
||||||
}
|
} else if (probeType === 'tcpSocket') {
|
||||||
} else {
|
probe.tcpSocket = { port: config[port] }
|
||||||
lines.push(`${prefix} - ${item}`)
|
} else {
|
||||||
}
|
const c = config[cmd] || 'cat /tmp/healthy'
|
||||||
}
|
probe.exec = { command: c.split(' ') }
|
||||||
} else if (typeof value === 'object') {
|
}
|
||||||
lines.push(`${prefix}${key}:`)
|
|
||||||
lines.push(toYaml(value, indent + 1))
|
probe.initialDelaySeconds = config[initDelay]
|
||||||
|
probe.periodSeconds = config[period]
|
||||||
|
probe.timeoutSeconds = config[timeout]
|
||||||
|
probe.failureThreshold = config[failure]
|
||||||
|
if (success !== null) probe.successThreshold = config[success]
|
||||||
|
return probe
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAffinity(config) {
|
||||||
|
const affinity = {}
|
||||||
|
|
||||||
|
// Node Affinity
|
||||||
|
if (config.enableNodeAffinity) {
|
||||||
|
const key = config.nodeAffinityKey1
|
||||||
|
const op = config.nodeAffinityOperator1
|
||||||
|
const vals = (config.nodeAffinityValues1 || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||||
|
const term = { matchExpressions: [{ key, operator: op }] }
|
||||||
|
if (['In', 'NotIn'].includes(op)) term.matchExpressions[0].values = vals
|
||||||
|
|
||||||
|
if (config.nodeAffinityType === 'required') {
|
||||||
|
affinity.nodeAffinity = { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [term] } }
|
||||||
} else {
|
} else {
|
||||||
lines.push(`${prefix}${key}: ${value}`)
|
affinity.nodeAffinity = {
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||||
|
weight: config.nodeAffinityWeight || 80,
|
||||||
|
preference: term,
|
||||||
|
}],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.join('\n')
|
// Pod Anti-Affinity
|
||||||
|
if (config.enablePodAntiAffinity) {
|
||||||
|
const labelKey = config.podAntiAffinityKey || 'app'
|
||||||
|
const topology = config.podAntiAffinityTopology || 'kubernetes.io/hostname'
|
||||||
|
const term = {
|
||||||
|
labelSelector: { matchExpressions: [{ key: labelKey, operator: 'In', values: [config.appLabel || config.appName] }] },
|
||||||
|
topologyKey: topology,
|
||||||
|
}
|
||||||
|
if (config.podAntiAffinityType === 'required') {
|
||||||
|
affinity.podAntiAffinity = { requiredDuringSchedulingIgnoredDuringExecution: [term] }
|
||||||
|
} else {
|
||||||
|
affinity.podAntiAffinity = {
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution: [{
|
||||||
|
weight: config.podAntiAffinityWeight || 100,
|
||||||
|
podAffinityTerm: term,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(affinity).length ? affinity : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateK8sStatefulSetYaml(config) {
|
export function generateK8sStatefulSetYaml(config) {
|
||||||
const labels = { app: config.appName }
|
const labels = { app: config.appLabel || config.appName }
|
||||||
|
if (config.versionLabel) labels.version = config.versionLabel
|
||||||
|
|
||||||
const container = {
|
const container = {
|
||||||
name: config.appName,
|
name: config.appName,
|
||||||
image: config.image,
|
image: config.image,
|
||||||
ports: [{ containerPort: config.containerPort, name: 'http' }],
|
imagePullPolicy: config.imagePullPolicy,
|
||||||
|
ports: [{ name: config.portName, containerPort: config.containerPort, protocol: config.protocol }],
|
||||||
resources: {
|
resources: {
|
||||||
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
|
requests: { cpu: config.cpuRequest, memory: config.memoryRequest },
|
||||||
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
|
limits: { cpu: config.cpuLimit, memory: config.memoryLimit },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 探针
|
||||||
|
container.startupProbe = buildProbe(config, 'enableStartupProbe', 'startupProbeType', 'startupPath', 'startupPort', 'startupCommand', null, 'startupInitialDelay', 'startupPeriod', 'startupTimeout', 'startupFailureThreshold', null)
|
||||||
|
container.livenessProbe = buildProbe(config, 'enableLivenessProbe', 'livenessProbeType', 'livenessPath', 'livenessPort', 'livenessCommand', 'livenessHttpHeaders', 'livenessInitialDelay', 'livenessPeriod', 'livenessTimeout', 'livenessFailureThreshold', 'livenessSuccessThreshold')
|
||||||
|
container.readinessProbe = buildProbe(config, 'enableReadinessProbe', 'readinessProbeType', 'readinessPath', 'readinessPort', 'readinessCommand', 'readinessHttpHeaders', 'readinessInitialDelay', 'readinessPeriod', 'readinessTimeout', 'readinessFailureThreshold', 'readinessSuccessThreshold')
|
||||||
|
if (!container.startupProbe) delete container.startupProbe
|
||||||
|
if (!container.livenessProbe) delete container.livenessProbe
|
||||||
|
if (!container.readinessProbe) delete container.readinessProbe
|
||||||
|
|
||||||
|
// 环境变量
|
||||||
|
if (config.enableEnv && config.envVars) {
|
||||||
|
container.env = config.envVars.split('\n').filter(Boolean).map(line => {
|
||||||
|
const [k, ...v] = line.split('=')
|
||||||
|
return { name: k.trim(), value: v.join('=').trim() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (config.enableEnvFromConfigMap || config.enableEnvFromSecret) {
|
||||||
|
container.envFrom = []
|
||||||
|
if (config.enableEnvFromConfigMap) container.envFrom.push({ configMapRef: { name: config.envFromConfigMapName } })
|
||||||
|
if (config.enableEnvFromSecret) container.envFrom.push({ secretRef: { name: config.envFromSecretName } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security Context (container-level)
|
||||||
|
if (config.enableSecurityContext) {
|
||||||
|
container.securityContext = {
|
||||||
|
runAsUser: config.runAsUser,
|
||||||
|
runAsGroup: config.runAsGroup,
|
||||||
|
runAsNonRoot: config.runAsNonRoot,
|
||||||
|
readOnlyRootFilesystem: config.readOnlyRootFilesystem || undefined,
|
||||||
|
allowPrivilegeEscalation: config.allowPrivilegeEscalation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume mounts for PVC
|
||||||
const volumeMounts = []
|
const volumeMounts = []
|
||||||
const volumeClaimTemplates = []
|
const volumeClaimTemplates = []
|
||||||
|
|
||||||
@@ -265,25 +371,48 @@ export function generateK8sStatefulSetYaml(config) {
|
|||||||
container.volumeMounts = volumeMounts
|
container.volumeMounts = volumeMounts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const podSpec = { containers: [container] }
|
||||||
|
|
||||||
|
// ServiceAccount
|
||||||
|
if (config.serviceAccountName) podSpec.serviceAccountName = config.serviceAccountName
|
||||||
|
|
||||||
|
// 优雅终止
|
||||||
|
if (config.terminationGracePeriod) podSpec.terminationGracePeriodSeconds = config.terminationGracePeriod
|
||||||
|
|
||||||
|
// NodeSelector
|
||||||
|
if (config.enableNodeSelector && config.nodeSelectorKey) {
|
||||||
|
podSpec.nodeSelector = { [config.nodeSelectorKey]: config.nodeSelectorValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affinity
|
||||||
|
const affinity = buildAffinity(config)
|
||||||
|
if (affinity) podSpec.affinity = affinity
|
||||||
|
|
||||||
|
// Tolerations
|
||||||
|
if (config.enableToleration) {
|
||||||
|
const t = { key: config.tolerationKey, operator: config.tolerationOperator, effect: config.tolerationEffect }
|
||||||
|
if (config.tolerationOperator === 'Equal') t.value = config.tolerationValue
|
||||||
|
if (config.tolerationEffect === 'NoExecute' && config.tolerationSeconds) t.tolerationSeconds = config.tolerationSeconds
|
||||||
|
podSpec.tolerations = [t]
|
||||||
|
}
|
||||||
|
|
||||||
|
// fsGroup
|
||||||
|
if (config.enableSecurityContext && config.fsGroup) {
|
||||||
|
podSpec.securityContext = { fsGroup: config.fsGroup }
|
||||||
|
}
|
||||||
|
|
||||||
const statefulset = {
|
const statefulset = {
|
||||||
apiVersion: 'apps/v1',
|
apiVersion: 'apps/v1',
|
||||||
kind: 'StatefulSet',
|
kind: 'StatefulSet',
|
||||||
metadata: {
|
metadata: { name: config.name, namespace: config.namespace, labels },
|
||||||
name: config.name,
|
|
||||||
namespace: config.namespace,
|
|
||||||
labels,
|
|
||||||
},
|
|
||||||
spec: {
|
spec: {
|
||||||
replicas: config.replicas,
|
replicas: config.replicas,
|
||||||
serviceName: config.serviceName,
|
serviceName: config.serviceName,
|
||||||
selector: { matchLabels: { app: config.appName } },
|
selector: { matchLabels: { app: config.appLabel || config.appName } },
|
||||||
updateStrategy: config.updateStrategy === 'RollingUpdate'
|
updateStrategy: config.updateStrategy === 'RollingUpdate'
|
||||||
? { type: 'RollingUpdate', rollingUpdate: { partition: config.partition } }
|
? { type: 'RollingUpdate', rollingUpdate: { partition: config.partition } }
|
||||||
: { type: 'OnDelete' },
|
: { type: 'OnDelete' },
|
||||||
template: {
|
template: { metadata: { labels }, spec: podSpec },
|
||||||
metadata: { labels },
|
|
||||||
spec: { containers: [container] },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +421,44 @@ export function generateK8sStatefulSetYaml(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const header = `# K8s StatefulSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'statefulset.yaml'}\n`
|
const header = `# K8s StatefulSet - 由 ConfTemplate 生成\n# 生成时间: ${new Date().toLocaleString('zh-CN')}\n# 部署命令: kubectl apply -f ${config.fileName || 'statefulset.yaml'}\n`
|
||||||
|
|
||||||
return header + '\n' + toYaml(statefulset)
|
return header + '\n' + toYaml(statefulset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toYaml(obj, indent = 0) {
|
||||||
|
const lines = []
|
||||||
|
const prefix = ' '.repeat(indent)
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
if (value === null || value === undefined || value === '') continue
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
lines.push(`${prefix}${key}:`)
|
||||||
|
for (const item of value) {
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||||
|
if (entries.length === 0) continue
|
||||||
|
const first = entries[0]
|
||||||
|
lines.push(`${prefix} - ${first[0]}: ${typeof first[1] === 'object' ? '' : first[1]}`)
|
||||||
|
if (typeof first[1] === 'object') {
|
||||||
|
lines.push(toYaml(first[1], indent + 3))
|
||||||
|
}
|
||||||
|
for (let i = 1; i < entries.length; i++) {
|
||||||
|
const [k, v] = entries[i]
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
lines.push(`${prefix} ${k}:`)
|
||||||
|
lines.push(toYaml(v, indent + 4))
|
||||||
|
} else {
|
||||||
|
lines.push(`${prefix} ${k}: ${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(`${prefix} - ${item}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
lines.push(`${prefix}${key}:`)
|
||||||
|
lines.push(toYaml(value, indent + 1))
|
||||||
|
} else {
|
||||||
|
lines.push(`${prefix}${key}: ${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
export const keepalivedSchema = {
|
||||||
|
id: 'keepalived',
|
||||||
|
name: 'Keepalived',
|
||||||
|
icon: 'Connection',
|
||||||
|
category: '高可用',
|
||||||
|
description: '基于 VRRP 协议的高可用解决方案,用于 IP 漂移和故障转移',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'keepalived.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: 'VRRP 实例配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'vrrpInstanceName',
|
||||||
|
label: 'VRRP 实例名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'VI_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'interface',
|
||||||
|
label: '网络接口',
|
||||||
|
type: 'text',
|
||||||
|
default: 'eth0',
|
||||||
|
tip: '绑定的网络接口名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'state',
|
||||||
|
label: '初始状态',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'MASTER - 主节点', value: 'master' },
|
||||||
|
{ label: 'BACKUP - 备节点', value: 'backup' },
|
||||||
|
],
|
||||||
|
default: 'master',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'virtualRouterId',
|
||||||
|
label: '虚拟路由 ID',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 255,
|
||||||
|
default: 51,
|
||||||
|
tip: '同一组的节点必须相同',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'priority',
|
||||||
|
label: '优先级',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 255,
|
||||||
|
default: 100,
|
||||||
|
tip: '数值越大优先级越高',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'advertInt',
|
||||||
|
label: '通告间隔 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 255,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '认证配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'authType',
|
||||||
|
label: '认证类型',
|
||||||
|
type: 'text',
|
||||||
|
default: 'PASS',
|
||||||
|
tip: 'PASS 或 AH',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authPass',
|
||||||
|
label: '认证密码',
|
||||||
|
type: 'text',
|
||||||
|
default: '1234',
|
||||||
|
tip: '同一组的节点必须相同',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '虚拟 IP 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'virtualIpAddress',
|
||||||
|
label: '虚拟 IP 地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.100/24 dev eth0',
|
||||||
|
required: true,
|
||||||
|
tip: '格式: IP/MASK dev 接口',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通知脚本',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableNotify',
|
||||||
|
label: '启用通知脚本',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '状态切换时执行的脚本',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notifyMaster',
|
||||||
|
label: '切换为 Master 时执行',
|
||||||
|
type: 'text',
|
||||||
|
default: "'/etc/keepalived/notify.sh master'",
|
||||||
|
dependsOn: { key: 'enableNotify', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notifyBackup',
|
||||||
|
label: '切换为 Backup 时执行',
|
||||||
|
type: 'text',
|
||||||
|
default: "'/etc/keepalived/notify.sh backup'",
|
||||||
|
dependsOn: { key: 'enableNotify', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'notifyFault',
|
||||||
|
label: '进入 Fault 状态时执行',
|
||||||
|
type: 'text',
|
||||||
|
default: "'/etc/keepalived/notify.sh fault'",
|
||||||
|
dependsOn: { key: 'enableNotify', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '单播配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableVipUnicast',
|
||||||
|
label: '启用单播',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '使用单播代替组播通信',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unicastPeerIp',
|
||||||
|
label: '单播对端 IP',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.2',
|
||||||
|
dependsOn: { key: 'enableVipUnicast', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '健康检查',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableHealthCheck',
|
||||||
|
label: '启用健康检查',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckType',
|
||||||
|
label: '检查类型',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'TCP 端口检查', value: 'tcp' },
|
||||||
|
{ label: 'HTTP GET 检查', value: 'http_get' },
|
||||||
|
{ label: 'SSL GET 检查', value: 'ssl_get' },
|
||||||
|
{ label: 'SMTP 检查', value: 'smtp' },
|
||||||
|
{ label: 'MISC 自定义检查', value: 'misc_check' },
|
||||||
|
],
|
||||||
|
default: 'tcp',
|
||||||
|
dependsOn: { key: 'enableHealthCheck', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckUrl',
|
||||||
|
label: '检查 URL 路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/health',
|
||||||
|
dependsOn: { key: 'healthCheckType', valueIn: ['http_get', 'ssl_get'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckPort',
|
||||||
|
label: '检查端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
dependsOn: { key: 'enableHealthCheck', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckTimeout',
|
||||||
|
label: '超时时间 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 30,
|
||||||
|
default: 3,
|
||||||
|
dependsOn: { key: 'enableHealthCheck', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckRetry',
|
||||||
|
label: '重试次数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
default: 3,
|
||||||
|
dependsOn: { key: 'enableHealthCheck', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckDelayBeforeRetry',
|
||||||
|
label: '重试间隔 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 30,
|
||||||
|
default: 3,
|
||||||
|
dependsOn: { key: 'enableHealthCheck', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateKeepalivedConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# Keepalived 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// VRRP Script (health check)
|
||||||
|
if (config.enableHealthCheck) {
|
||||||
|
lines.push(`vrrp_script chk_service {`)
|
||||||
|
if (config.healthCheckType === 'tcp') {
|
||||||
|
lines.push(` script "</dev/tcp/127.0.0.1/${config.healthCheckPort}"`)
|
||||||
|
} else if (config.healthCheckType === 'http_get') {
|
||||||
|
lines.push(` script "curl -s http://127.0.0.1:${config.healthCheckPort}${config.healthCheckUrl}"`)
|
||||||
|
} else if (config.healthCheckType === 'ssl_get') {
|
||||||
|
lines.push(` script "curl -sk https://127.0.0.1:${config.healthCheckPort}${config.healthCheckUrl}"`)
|
||||||
|
} else if (config.healthCheckType === 'smtp') {
|
||||||
|
lines.push(` script "nc -w ${config.healthCheckTimeout} 127.0.0.1 25 < /dev/null"`)
|
||||||
|
} else {
|
||||||
|
lines.push(` script "/etc/keepalived/check_script.sh"`)
|
||||||
|
}
|
||||||
|
lines.push(` interval ${config.healthCheckTimeout}`)
|
||||||
|
lines.push(` weight -20`)
|
||||||
|
lines.push(` fall ${config.healthCheckRetry}`)
|
||||||
|
lines.push(` rise ${config.healthCheckRetry}`)
|
||||||
|
lines.push(`}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VRRP Instance
|
||||||
|
lines.push(`vrrp_instance ${config.vrrpInstanceName} {`)
|
||||||
|
lines.push(` state ${config.state.toUpperCase()}`)
|
||||||
|
lines.push(` interface ${config.interface}`)
|
||||||
|
lines.push(` virtual_router_id ${config.virtualRouterId}`)
|
||||||
|
lines.push(` priority ${config.priority}`)
|
||||||
|
lines.push(` advert_int ${config.advertInt}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
lines.push(` authentication {`)
|
||||||
|
lines.push(` auth_type ${config.authType}`)
|
||||||
|
lines.push(` auth_pass ${config.authPass}`)
|
||||||
|
lines.push(` }`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Virtual IP
|
||||||
|
lines.push(` virtual_ipaddress {`)
|
||||||
|
lines.push(` ${config.virtualIpAddress}`)
|
||||||
|
lines.push(` }`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Unicast
|
||||||
|
if (config.enableVipUnicast) {
|
||||||
|
lines.push(` unicast_src_ip ${config.virtualIpAddress.split('/')[0].trim()}`)
|
||||||
|
lines.push(` unicast_peer {`)
|
||||||
|
lines.push(` ${config.unicastPeerIp}`)
|
||||||
|
lines.push(` }`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
if (config.enableNotify) {
|
||||||
|
lines.push(` notify_master ${config.notifyMaster}`)
|
||||||
|
lines.push(` notify_backup ${config.notifyBackup}`)
|
||||||
|
lines.push(` notify_fault ${config.notifyFault}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check track
|
||||||
|
if (config.enableHealthCheck) {
|
||||||
|
lines.push(` track_script {`)
|
||||||
|
lines.push(` chk_service`)
|
||||||
|
lines.push(` }`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`}`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
export const lvsSchema = {
|
||||||
|
id: 'lvs',
|
||||||
|
name: 'LVS',
|
||||||
|
icon: 'Connection',
|
||||||
|
category: '高可用',
|
||||||
|
description: 'Linux Virtual Server,基于 IP 层的四层负载均衡',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'lvs-dr.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '虚拟服务配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'vip',
|
||||||
|
label: '虚拟 IP (VIP)',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.100',
|
||||||
|
required: true,
|
||||||
|
tip: '对外提供服务的虚拟 IP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vipPort',
|
||||||
|
label: '虚拟服务端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'protocol',
|
||||||
|
label: '协议',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'TCP', value: 'tcp' },
|
||||||
|
{ label: 'UDP', value: 'udp' },
|
||||||
|
{ label: 'FTP', value: 'ftp' },
|
||||||
|
],
|
||||||
|
default: 'tcp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'scheduler',
|
||||||
|
label: '调度算法',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'wrr - 加权轮询', value: 'wrr' },
|
||||||
|
{ label: 'wlc - 加权最少连接 (推荐)', value: 'wlc' },
|
||||||
|
{ label: 'lc - 最少连接', value: 'lc' },
|
||||||
|
{ label: 'sh - 源地址哈希', value: 'sh' },
|
||||||
|
{ label: 'dh - 目标地址哈希', value: 'dh' },
|
||||||
|
{ label: 'rr - 轮询', value: 'rr' },
|
||||||
|
],
|
||||||
|
default: 'wrr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'drMode',
|
||||||
|
label: '转发模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'DR - 直接路由 (推荐)', value: 'dr' },
|
||||||
|
{ label: 'NAT - 网络地址转换', value: 'nat' },
|
||||||
|
{ label: 'TUN - IP 隧道', value: 'tun' },
|
||||||
|
],
|
||||||
|
default: 'dr',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Real Server 1',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'realServer1',
|
||||||
|
label: '真实服务器 1 IP',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer1Port',
|
||||||
|
label: '端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer1Weight',
|
||||||
|
label: '权重',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 256,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Real Server 2',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'realServer2',
|
||||||
|
label: '真实服务器 2 IP',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.11',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer2Port',
|
||||||
|
label: '端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer2Weight',
|
||||||
|
label: '权重',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 256,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Real Server 3 (可选)',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableRealServer3',
|
||||||
|
label: '启用第三台服务器',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer3',
|
||||||
|
label: '真实服务器 3 IP',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.12',
|
||||||
|
dependsOn: { key: 'enableRealServer3', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer3Port',
|
||||||
|
label: '端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
dependsOn: { key: 'enableRealServer3', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realServer3Weight',
|
||||||
|
label: '权重',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 256,
|
||||||
|
default: 1,
|
||||||
|
dependsOn: { key: 'enableRealServer3', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '健康检查',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableHealthCheck',
|
||||||
|
label: '启用健康检查',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckType',
|
||||||
|
label: '检查类型',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'TCP 端口检查', value: 'tcp' },
|
||||||
|
{ label: 'HTTP GET 检查', value: 'http' },
|
||||||
|
],
|
||||||
|
default: 'tcp',
|
||||||
|
dependsOn: { key: 'enableHealthCheck', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckUrl',
|
||||||
|
label: '检查 URL 路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/health',
|
||||||
|
dependsOn: { key: 'healthCheckType', value: 'http' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'healthCheckPort',
|
||||||
|
label: '检查端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 80,
|
||||||
|
dependsOn: { key: 'healthCheckType', value: 'http' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateLvsConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`#!/bin/bash`)
|
||||||
|
lines.push(`# LVS 配置脚本 (ipvsadm 命令) - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(`# 转发模式: ${config.drMode.toUpperCase()}`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`# 清除现有规则`)
|
||||||
|
lines.push(`ipvsadm -C`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Add VIP
|
||||||
|
const modeFlag = config.drMode === 'nat' ? '-m' : config.drMode === 'tun' ? '-i' : '-g'
|
||||||
|
lines.push(`# 添加虚拟服务`)
|
||||||
|
lines.push(`# VIP: ${config.vip}:${config.vipPort}`)
|
||||||
|
lines.push(`# 协议: ${config.protocol.toUpperCase()}`)
|
||||||
|
lines.push(`# 调度算法: ${config.scheduler}`)
|
||||||
|
lines.push(`ipvsadm -A -t ${config.vip}:${config.vipPort} -s ${config.scheduler}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Add real servers
|
||||||
|
lines.push(`# 添加真实服务器`)
|
||||||
|
lines.push(`# Real Server 1: ${config.realServer1}:${config.realServer1Port} (weight=${config.realServer1Weight})`)
|
||||||
|
lines.push(`ipvsadm -a -t ${config.vip}:${config.vipPort} -r ${config.realServer1}:${config.realServer1Port} -w ${config.realServer1Weight} ${modeFlag}`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`# Real Server 2: ${config.realServer2}:${config.realServer2Port} (weight=${config.realServer2Weight})`)
|
||||||
|
lines.push(`ipvsadm -a -t ${config.vip}:${config.vipPort} -r ${config.realServer2}:${config.realServer2Port} -w ${config.realServer2Weight} ${modeFlag}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
if (config.enableRealServer3) {
|
||||||
|
lines.push(`# Real Server 3: ${config.realServer3}:${config.realServer3Port} (weight=${config.realServer3Weight})`)
|
||||||
|
lines.push(`ipvsadm -a -t ${config.vip}:${config.vipPort} -r ${config.realServer3}:${config.realServer3Port} -w ${config.realServer3Weight} ${modeFlag}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`# 查看当前规则`)
|
||||||
|
lines.push(`ipvsadm -Ln`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Health check script
|
||||||
|
if (config.enableHealthCheck) {
|
||||||
|
lines.push(`# ============================================================`)
|
||||||
|
lines.push(`# 健康检查脚本 (保存为 /etc/lvs/check_real_server.sh)`)
|
||||||
|
lines.push(`# ============================================================`)
|
||||||
|
lines.push(`cat > /etc/lvs/check_real_server.sh << 'HEALTHCHECK_EOF'`)
|
||||||
|
lines.push(`#!/bin/bash`)
|
||||||
|
lines.push(`# LVS 健康检查脚本`)
|
||||||
|
lines.push(`VIP="${config.vip}"`)
|
||||||
|
lines.push(`VPORT="${config.vipPort}"`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
const servers = [
|
||||||
|
{ ip: config.realServer1, port: config.realServer1Port, weight: config.realServer1Weight },
|
||||||
|
{ ip: config.realServer2, port: config.realServer2Port, weight: config.realServer2Weight },
|
||||||
|
]
|
||||||
|
if (config.enableRealServer3) {
|
||||||
|
servers.push({ ip: config.realServer3, port: config.realServer3Port, weight: config.realServer3Weight })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < servers.length; i++) {
|
||||||
|
const rs = servers[i]
|
||||||
|
lines.push(`# Check Real Server ${i + 1}: ${rs.ip}`)
|
||||||
|
if (config.healthCheckType === 'tcp') {
|
||||||
|
lines.push(`if nc -z -w 3 ${rs.ip} ${rs.port} > /dev/null 2>&1; then`)
|
||||||
|
} else {
|
||||||
|
lines.push(`if curl -s -o /dev/null -w "%{http_code}" http://${rs.ip}:${config.healthCheckPort}${config.healthCheckUrl} | grep -q "200"; then`)
|
||||||
|
}
|
||||||
|
lines.push(` # Server ${rs.ip} is UP`)
|
||||||
|
lines.push(` ipvsadm -a -t $VIP:$VPORT -r ${rs.ip}:${rs.port} -w ${rs.weight} ${modeFlag} 2>/dev/null`)
|
||||||
|
lines.push(`else`)
|
||||||
|
lines.push(` # Server ${rs.ip} is DOWN, removing from pool`)
|
||||||
|
lines.push(` ipvsadm -d -t $VIP:$VPORT -r ${rs.ip}:${rs.port} 2>/dev/null`)
|
||||||
|
lines.push(`fi`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`HEALTHCHECK_EOF`)
|
||||||
|
lines.push(`chmod +x /etc/lvs/check_real_server.sh`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`# 添加 crontab 定时检查 (每 10 秒)`)
|
||||||
|
lines.push(`# */1 * * * * /etc/lvs/check_real_server.sh`)
|
||||||
|
lines.push(`# */1 * * * * sleep 10 && /etc/lvs/check_real_server.sh`)
|
||||||
|
lines.push(`# */1 * * * * sleep 20 && /etc/lvs/check_real_server.sh`)
|
||||||
|
lines.push(`# */1 * * * * sleep 30 && /etc/lvs/check_real_server.sh`)
|
||||||
|
lines.push(`# */1 * * * * sleep 40 && /etc/lvs/check_real_server.sh`)
|
||||||
|
lines.push(`# */1 * * * * sleep 50 && /etc/lvs/check_real_server.sh`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
export const mycatSchema = {
|
||||||
|
id: 'mycat',
|
||||||
|
name: 'Mycat',
|
||||||
|
icon: 'Coin',
|
||||||
|
category: '数据库中间件',
|
||||||
|
description: '开源数据库中间件,支持 MySQL 协议',
|
||||||
|
format: 'xml',
|
||||||
|
fileName: 'server.xml',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '网络配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'port',
|
||||||
|
label: '服务端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 8066,
|
||||||
|
tip: 'MyCat 数据服务端口',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'managerPort',
|
||||||
|
label: '管理端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 9066,
|
||||||
|
tip: 'MyCat 管理服务端口',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'processors',
|
||||||
|
label: '处理线程数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 64,
|
||||||
|
default: 8,
|
||||||
|
tip: '建议设为 CPU 核心数',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxConnections',
|
||||||
|
label: '最大连接数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10000,
|
||||||
|
default: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'idleTimeout',
|
||||||
|
label: '空闲超时 (毫秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 3600000,
|
||||||
|
step: 1000,
|
||||||
|
default: 1800000,
|
||||||
|
tip: '连接空闲超时时间,默认 30 分钟',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据库配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'defaultSchema',
|
||||||
|
label: '默认 Schema',
|
||||||
|
type: 'text',
|
||||||
|
default: 'mydb',
|
||||||
|
tip: '客户端连接后默认使用的逻辑库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'charset',
|
||||||
|
label: '字符集',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'utf8mb4 (推荐)', value: 'utf8mb4' },
|
||||||
|
{ label: 'utf8', value: 'utf8' },
|
||||||
|
],
|
||||||
|
default: 'utf8mb4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'txIsolation',
|
||||||
|
label: '事务隔离级别',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: '1 - READ_UNCOMMITTED', value: '1' },
|
||||||
|
{ label: '2 - READ_COMMITTED (推荐)', value: '2' },
|
||||||
|
{ label: '3 - REPEATABLE_READ', value: '3' },
|
||||||
|
{ label: '4 - SERIALIZABLE', value: '4' },
|
||||||
|
],
|
||||||
|
default: '2',
|
||||||
|
tip: '1=READ_UNCOMMITTED 2=READ_COMMITTED 3=REPEATABLE_READ 4=SERIALIZABLE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sqlExecuteLimit',
|
||||||
|
label: 'SQL 执行限制',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 10000,
|
||||||
|
default: 100,
|
||||||
|
tip: '单次查询最大执行行数,0 表示不限制',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '性能与优化',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableGather',
|
||||||
|
label: '统计 SQL 执行',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '统计 SQL 执行次数和时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'useCompression',
|
||||||
|
label: '启用压缩',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: 'MySQL 协议压缩,减少网络传输',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户认证',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'authUser',
|
||||||
|
label: '用户名',
|
||||||
|
type: 'text',
|
||||||
|
default: 'root',
|
||||||
|
tip: '连接 MyCat 的用户名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authPassword',
|
||||||
|
label: '密码',
|
||||||
|
type: 'text',
|
||||||
|
default: '123456',
|
||||||
|
tip: '连接 MyCat 的密码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'readOnly',
|
||||||
|
label: '只读用户',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '该用户是否只有读权限',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMycatServerXml(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||||
|
lines.push(`<!-- MyCat server.xml 配置文件 - 由 ConfTemplate 生成 -->`)
|
||||||
|
lines.push(`<!-- 生成时间: ${new Date().toLocaleString('zh-CN')} -->`)
|
||||||
|
lines.push(`<!DOCTYPE mycat:server SYSTEM "server.dtd">`)
|
||||||
|
lines.push(`<mycat:server xmlns:mycat="http://io.mycat/">`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 系统配置
|
||||||
|
lines.push(` <system>`)
|
||||||
|
lines.push(` <property name="serverPort">${config.port}</property>`)
|
||||||
|
lines.push(` <property name="managerPort">${config.managerPort}</property>`)
|
||||||
|
lines.push(` <property name="processors">${config.processors}</property>`)
|
||||||
|
lines.push(` <property name="maxConnections">${config.maxConnections}</property>`)
|
||||||
|
lines.push(` <property name="idleTimeout">${config.idleTimeout}</property>`)
|
||||||
|
lines.push(` <property name="defaultSchema">${config.defaultSchema}</property>`)
|
||||||
|
lines.push(` <property name="characterEncoding">${config.charset}</property>`)
|
||||||
|
lines.push(` <property name="txIsolation">${config.txIsolation}</property>`)
|
||||||
|
lines.push(` <property name="sqlExecuteTimeout">${config.sqlExecuteLimit}</property>`)
|
||||||
|
|
||||||
|
if (config.enableGather) {
|
||||||
|
lines.push(` <property name="useSqlStat">1</property>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.useCompression) {
|
||||||
|
lines.push(` <property name="handleDistributedTransactions">0</property>`)
|
||||||
|
lines.push(` <property name="useGlobleTableCheck">0</property>`)
|
||||||
|
lines.push(` <!-- 压缩配置 -->`)
|
||||||
|
lines.push(` <property name="packetHeaderSize">4</property>`)
|
||||||
|
lines.push(` <property name="maxPacketSize">16777216</property>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` </system>`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 用户配置
|
||||||
|
lines.push(` <!-- 用户配置 -->`)
|
||||||
|
lines.push(` <user name="${config.authUser}">`)
|
||||||
|
lines.push(` <property name="password">${config.authPassword}</property>`)
|
||||||
|
lines.push(` <property name="schemas">${config.defaultSchema}</property>`)
|
||||||
|
|
||||||
|
if (config.readOnly) {
|
||||||
|
lines.push(` <property name="readOnly">true</property>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` </user>`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 集群配置注释
|
||||||
|
lines.push(` <!--`)
|
||||||
|
lines.push(` 集群配置示例 (在其他节点配置):`)
|
||||||
|
lines.push(` <cluster>`)
|
||||||
|
lines.push(` <node name="node1" ip="192.168.1.101" port="3306"/>`)
|
||||||
|
lines.push(` <node name="node2" ip="192.168.1.102" port="3306"/>`)
|
||||||
|
lines.push(` </cluster>`)
|
||||||
|
lines.push(` -->`)
|
||||||
|
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 连接池注释
|
||||||
|
lines.push(` <!-- 连接池配置示例 (在 schema.xml 中配置): -->`)
|
||||||
|
lines.push(` <!--`)
|
||||||
|
lines.push(` <dataHost name="localhost1" maxCon="1000" minCon="10" balance="0"`)
|
||||||
|
lines.push(` writeType="0" dbType="mysql" dbDriver="native">`)
|
||||||
|
lines.push(` <heartbeat>select user()</heartbeat>`)
|
||||||
|
lines.push(` <writeHost host="hostM1" url="192.168.1.101:3306"`)
|
||||||
|
lines.push(` user="root" password="123456"/>`)
|
||||||
|
lines.push(` </dataHost>`)
|
||||||
|
lines.push(` -->`)
|
||||||
|
|
||||||
|
lines.push(`</mycat:server>`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
export const nfsSchema = {
|
||||||
|
id: 'nfs',
|
||||||
|
name: 'NFS',
|
||||||
|
icon: 'Files',
|
||||||
|
category: '系统服务',
|
||||||
|
description: 'NFS 网络文件系统 exports 配置',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'exports',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '主共享配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'sharePath',
|
||||||
|
label: '共享路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/data/share',
|
||||||
|
tip: '要共享的本地目录路径',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientAddress',
|
||||||
|
label: '客户端地址',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.0/24',
|
||||||
|
tip: '允许访问的客户端网段或主机',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'permissions',
|
||||||
|
label: '权限',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'rw - 读写', value: 'rw' },
|
||||||
|
{ label: 'ro - 只读', value: 'ro' },
|
||||||
|
],
|
||||||
|
default: 'rw',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rootSquash',
|
||||||
|
label: 'Root Squash',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '将 root 用户映射为匿名用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'allSquash',
|
||||||
|
label: 'All Squash',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '将所有用户映射为匿名用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sync',
|
||||||
|
label: '同步写入',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '同步写入磁盘,更安全但稍慢',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'anonuid',
|
||||||
|
label: '匿名用户 UID',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 65535,
|
||||||
|
default: 65534,
|
||||||
|
tip: '匿名用户的 UID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'anongid',
|
||||||
|
label: '匿名用户 GID',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 65535,
|
||||||
|
default: 65534,
|
||||||
|
tip: '匿名用户的 GID',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '附加共享',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'additionalShares',
|
||||||
|
label: '启用附加共享',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '配置第二个 NFS 共享目录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sharePath2',
|
||||||
|
label: '共享路径 2',
|
||||||
|
type: 'text',
|
||||||
|
default: '/data/backup',
|
||||||
|
dependsOn: { key: 'additionalShares', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'clientAddress2',
|
||||||
|
label: '客户端地址 2',
|
||||||
|
type: 'text',
|
||||||
|
default: '10.0.0.0/24',
|
||||||
|
dependsOn: { key: 'additionalShares', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'permissions2',
|
||||||
|
label: '权限 2',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'rw - 读写', value: 'rw' },
|
||||||
|
{ label: 'ro - 只读', value: 'ro' },
|
||||||
|
],
|
||||||
|
default: 'ro',
|
||||||
|
dependsOn: { key: 'additionalShares', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NFS 版本',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableNfsV4',
|
||||||
|
label: '启用 NFSv4',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '使用 NFS 第四版协议',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateNfsExports(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# NFS exports 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(`# 格式: <目录> <客户端>(<选项>)`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Build options string for primary share
|
||||||
|
const buildOptions = (cfg, prefix = '') => {
|
||||||
|
const perms = cfg[`permissions${prefix}`] || 'rw'
|
||||||
|
const rootSquash = cfg.rootSquash !== undefined ? cfg.rootSquash : true
|
||||||
|
const allSquash = cfg.allSquash !== undefined ? cfg.allSquash : false
|
||||||
|
const sync = cfg.sync !== undefined ? cfg.sync : true
|
||||||
|
|
||||||
|
const opts = []
|
||||||
|
opts.push(perms)
|
||||||
|
opts.push(sync ? 'sync' : 'async')
|
||||||
|
opts.push(rootSquash ? 'root_squash' : 'no_root_squash')
|
||||||
|
if (allSquash) {
|
||||||
|
opts.push('all_squash')
|
||||||
|
}
|
||||||
|
opts.push(`anonuid=${cfg.anonuid || 65534}`)
|
||||||
|
opts.push(`anongid=${cfg.anongid || 65534}`)
|
||||||
|
|
||||||
|
return opts.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary share
|
||||||
|
lines.push(`# ======================== 主共享 ========================`)
|
||||||
|
lines.push(`${config.sharePath} ${config.clientAddress}(${buildOptions(config)})`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Additional share
|
||||||
|
if (config.additionalShares) {
|
||||||
|
lines.push(`# ======================== 附加共享 ========================`)
|
||||||
|
const opts2 = []
|
||||||
|
opts2.push(config.permissions2 || 'ro')
|
||||||
|
opts2.push('sync')
|
||||||
|
opts2.push('root_squash')
|
||||||
|
opts2.push(`anonuid=${config.anonuid || 65534}`)
|
||||||
|
opts2.push(`anongid=${config.anongid || 65534}`)
|
||||||
|
lines.push(`${config.sharePath2} ${config.clientAddress2}(${opts2.join(',')})`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NFSv4 note
|
||||||
|
if (config.enableNfsV4) {
|
||||||
|
lines.push(`# ======================== NFSv4 配置提示 ========================`)
|
||||||
|
lines.push(`# NFSv4 需要在 /etc/exports 中使用 fsid=0 或 fsid=N 来定义导出根`)
|
||||||
|
lines.push(`# 确保 rpcbind 和 nfs-server 服务已启动:`)
|
||||||
|
lines.push(`# systemctl enable --now rpcbind nfs-server`)
|
||||||
|
lines.push(`# 修改后执行: exportfs -ra`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
+156
-6
@@ -192,6 +192,105 @@ export const nginxSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '限流配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableLimitReq',
|
||||||
|
label: '启用请求限流',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '限制客户端请求速率,防止恶意请求',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'limitReqZone',
|
||||||
|
label: '限流区域配置',
|
||||||
|
type: 'text',
|
||||||
|
default: "'$binary_remote_addr' zone=one:10m rate=10r/s",
|
||||||
|
tip: 'limit_zone 参数,如 $binary_remote_addr zone=one:10m rate=10r/s',
|
||||||
|
dependsOn: { key: 'enableLimitReq', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'limitReqBurst',
|
||||||
|
label: '突发请求数',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 1000,
|
||||||
|
default: 20,
|
||||||
|
tip: '允许超出 rate 的突发请求数(burst)',
|
||||||
|
dependsOn: { key: 'enableLimitReq', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Upstream 负载均衡',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableUpstream',
|
||||||
|
label: '启用 Upstream',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '配置后端服务器负载均衡组',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'upstreamName',
|
||||||
|
label: 'Upstream 名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'backend',
|
||||||
|
tip: 'upstream 块名称,用于 proxy_pass 引用',
|
||||||
|
dependsOn: { key: 'enableUpstream', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'upstreamServers',
|
||||||
|
label: '后端服务器列表',
|
||||||
|
type: 'text',
|
||||||
|
default: "server 127.0.0.1:8080 weight=3\nserver 127.0.0.1:8081 weight=2",
|
||||||
|
tip: '每行一个 server 指令,支持 weight/down/backup 参数',
|
||||||
|
dependsOn: { key: 'enableUpstream', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'upstreamMethod',
|
||||||
|
label: '负载均衡算法',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'round-robin - 轮询 (默认)', value: 'round-robin' },
|
||||||
|
{ label: 'least_conn - 最少连接', value: 'least_conn' },
|
||||||
|
{ label: 'ip_hash - IP 哈希', value: 'ip_hash' },
|
||||||
|
],
|
||||||
|
default: 'least_conn',
|
||||||
|
tip: '请求分发策略',
|
||||||
|
dependsOn: { key: 'enableUpstream', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '缓存配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableCache',
|
||||||
|
label: '启用代理缓存',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '缓存后端响应以提升性能',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cachePath',
|
||||||
|
label: '缓存路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m',
|
||||||
|
tip: 'proxy_cache_path 参数',
|
||||||
|
dependsOn: { key: 'enableCache', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cacheBypass',
|
||||||
|
label: '缓存绕过条件',
|
||||||
|
type: 'text',
|
||||||
|
default: '$cookie_nocache',
|
||||||
|
tip: 'proxy_cache_bypass 条件,满足时跳过缓存',
|
||||||
|
dependsOn: { key: 'enableCache', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,11 +343,45 @@ export function generateNginxConf(config) {
|
|||||||
lines.push(``)
|
lines.push(``)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`)
|
// 限流配置
|
||||||
lines.push(` # upstream backend {`)
|
if (config.enableLimitReq) {
|
||||||
lines.push(` # server 127.0.0.1:8080;`)
|
lines.push(` # 限流配置`)
|
||||||
lines.push(` # }`)
|
lines.push(` limit_req_zone ${config.limitReqZone};`)
|
||||||
lines.push(``)
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream 负载均衡
|
||||||
|
if (config.enableUpstream) {
|
||||||
|
lines.push(` # Upstream 负载均衡`)
|
||||||
|
if (config.upstreamMethod && config.upstreamMethod !== 'round-robin') {
|
||||||
|
lines.push(` upstream ${config.upstreamName} {`)
|
||||||
|
lines.push(` ${config.upstreamMethod};`)
|
||||||
|
} else {
|
||||||
|
lines.push(` upstream ${config.upstreamName} {`)
|
||||||
|
}
|
||||||
|
const servers = config.upstreamServers.split('\n').filter(s => s.trim())
|
||||||
|
servers.forEach(server => {
|
||||||
|
lines.push(` ${server.trim()}`)
|
||||||
|
})
|
||||||
|
lines.push(` }`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存配置
|
||||||
|
if (config.enableCache) {
|
||||||
|
lines.push(` # 代理缓存配置`)
|
||||||
|
lines.push(` proxy_cache_path ${config.cachePath};`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upstream fallback when not enabled
|
||||||
|
if (!config.enableUpstream) {
|
||||||
|
lines.push(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`)
|
||||||
|
lines.push(` # upstream backend {`)
|
||||||
|
lines.push(` # server 127.0.0.1:8080;`)
|
||||||
|
lines.push(` # }`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
lines.push(` server {`)
|
lines.push(` server {`)
|
||||||
lines.push(` listen ${config.httpPort};`)
|
lines.push(` listen ${config.httpPort};`)
|
||||||
@@ -308,10 +441,19 @@ export function generateNginxConf(config) {
|
|||||||
lines.push(``)
|
lines.push(``)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 限流应用到 location
|
||||||
lines.push(` location / {`)
|
lines.push(` location / {`)
|
||||||
|
|
||||||
|
if (config.enableLimitReq) {
|
||||||
|
lines.push(` limit_req zone=one burst=${config.limitReqBurst} nodelay;`)
|
||||||
|
}
|
||||||
|
|
||||||
if (config.enableProxy) {
|
if (config.enableProxy) {
|
||||||
lines.push(` proxy_pass ${config.proxyTarget};`)
|
if (config.enableUpstream) {
|
||||||
|
lines.push(` proxy_pass http://${config.upstreamName};`)
|
||||||
|
} else {
|
||||||
|
lines.push(` proxy_pass ${config.proxyTarget};`)
|
||||||
|
}
|
||||||
lines.push(` proxy_http_version 1.1;`)
|
lines.push(` proxy_http_version 1.1;`)
|
||||||
lines.push(` proxy_set_header Upgrade $http_upgrade;`)
|
lines.push(` proxy_set_header Upgrade $http_upgrade;`)
|
||||||
lines.push(` proxy_set_header Connection 'upgrade';`)
|
lines.push(` proxy_set_header Connection 'upgrade';`)
|
||||||
@@ -320,6 +462,14 @@ export function generateNginxConf(config) {
|
|||||||
lines.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
|
lines.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
|
||||||
lines.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
|
lines.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
|
||||||
lines.push(` proxy_cache_bypass $http_upgrade;`)
|
lines.push(` proxy_cache_bypass $http_upgrade;`)
|
||||||
|
|
||||||
|
if (config.enableCache) {
|
||||||
|
lines.push(` proxy_cache my_cache;`)
|
||||||
|
lines.push(` proxy_cache_valid 200 302 10m;`)
|
||||||
|
lines.push(` proxy_cache_valid 404 1m;`)
|
||||||
|
lines.push(` proxy_cache_bypass ${config.cacheBypass};`)
|
||||||
|
lines.push(` add_header X-Cache-Status $upstream_cache_status;`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(` root /usr/share/nginx/html;`)
|
lines.push(` root /usr/share/nginx/html;`)
|
||||||
lines.push(` index index.html index.htm;`)
|
lines.push(` index index.html index.htm;`)
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
export const ntpSchema = {
|
||||||
|
id: 'ntp',
|
||||||
|
name: 'NTP',
|
||||||
|
icon: 'Timer',
|
||||||
|
category: '系统服务',
|
||||||
|
description: 'NTP 网络时间协议服务配置',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'ntp.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '时间服务器',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'server',
|
||||||
|
label: 'NTP 服务器',
|
||||||
|
type: 'text',
|
||||||
|
default: 'pool.ntp.org',
|
||||||
|
tip: '主 NTP 服务器地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'server2',
|
||||||
|
label: '备用 NTP 服务器',
|
||||||
|
type: 'text',
|
||||||
|
default: 'cn.ntp.org.cn',
|
||||||
|
tip: '备用 NTP 服务器地址',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '访问控制',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'restrict',
|
||||||
|
label: 'IPv4 访问限制',
|
||||||
|
type: 'text',
|
||||||
|
default: '-4 default kod notrap nomodify nopeer noquery limited',
|
||||||
|
tip: 'IPv4 默认访问控制规则',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'restrict6',
|
||||||
|
label: 'IPv6 访问限制',
|
||||||
|
type: 'text',
|
||||||
|
default: '-6 default kod notrap nomodify nopeer noquery limited',
|
||||||
|
tip: 'IPv6 默认访问控制规则',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件路径',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'driftfile',
|
||||||
|
label: '漂移文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/lib/ntp/drift',
|
||||||
|
tip: '频率补偿文件路径',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logfile',
|
||||||
|
label: '日志文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/log/ntp.log',
|
||||||
|
tip: 'NTP 日志文件路径',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '认证配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableAuth',
|
||||||
|
label: '启用认证',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '启用 NTP 认证机制',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'keysFile',
|
||||||
|
label: '密钥文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/ntp/keys',
|
||||||
|
dependsOn: { key: 'enableAuth', value: true },
|
||||||
|
tip: 'NTP 认证密钥文件',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '性能调优',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'tinkerPanic',
|
||||||
|
label: 'Tinker Panic 阈值',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 1000,
|
||||||
|
default: 0,
|
||||||
|
tip: '0=永不 panic,超过此秒数偏移将 panic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'burst',
|
||||||
|
label: '启用 Burst 模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '在网络正常时发送一批请求以提高精度',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'minpoll',
|
||||||
|
label: '最小轮询间隔 (2^n 秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 3,
|
||||||
|
max: 17,
|
||||||
|
default: 6,
|
||||||
|
tip: '最小轮询间隔指数,3=8秒',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxpoll',
|
||||||
|
label: '最大轮询间隔 (2^n 秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 3,
|
||||||
|
max: 17,
|
||||||
|
default: 10,
|
||||||
|
tip: '最大轮询间隔指数,10=1024秒',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateNtpConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# NTP 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Time servers
|
||||||
|
lines.push(`# ======================== 时间服务器 ========================`)
|
||||||
|
lines.push(`server ${config.server} iburst minpoll ${config.minpoll} maxpoll ${config.maxpoll}`)
|
||||||
|
lines.push(`server ${config.server2} iburst minpoll ${config.minpoll} maxpoll ${config.maxpoll}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Access control
|
||||||
|
lines.push(`# ======================== 访问控制 ========================`)
|
||||||
|
lines.push(`restrict ${config.restrict}`)
|
||||||
|
lines.push(`restrict ${config.restrict6}`)
|
||||||
|
lines.push(`restrict 127.0.0.1`)
|
||||||
|
lines.push(`restrict ::1`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// File paths
|
||||||
|
lines.push(`# ======================== 文件路径 ========================`)
|
||||||
|
lines.push(`driftfile ${config.driftfile}`)
|
||||||
|
lines.push(`logfile ${config.logfile}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
if (config.enableAuth) {
|
||||||
|
lines.push(`# ======================== 认证配置 ========================`)
|
||||||
|
lines.push(`enable auth`)
|
||||||
|
lines.push(`keys ${config.keysFile}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
lines.push(`# ======================== 性能调优 ========================`)
|
||||||
|
if (config.tinkerPanic > 0) {
|
||||||
|
lines.push(`tinker panic ${config.tinkerPanic}`)
|
||||||
|
} else {
|
||||||
|
lines.push(`tinker panic 0`)
|
||||||
|
}
|
||||||
|
if (config.burst) {
|
||||||
|
lines.push(`burst`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Leap second
|
||||||
|
lines.push(`# ======================== 其他 ========================`)
|
||||||
|
lines.push(`# 闰秒文件(如需要)`)
|
||||||
|
lines.push(`# leapfile /usr/share/zoneinfo/leap-seconds.list`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -180,6 +180,114 @@ export const postgresqlSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '连接池',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'superuserReservedConnections',
|
||||||
|
label: '超级用户保留连接数',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
default: 3,
|
||||||
|
tip: '为超级用户保留的连接数,确保管理员总能连入',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'idleInTransactionSessionTimeout',
|
||||||
|
label: '空闲事务超时 (毫秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 86400000,
|
||||||
|
default: 0,
|
||||||
|
tip: '0 = 禁用。空闲事务超过此时间将被终止,防止连接泄漏',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '流复制',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableReplication',
|
||||||
|
label: '启用流复制',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '配置此实例支持流复制(主库或备库)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hotStandby',
|
||||||
|
label: '热备模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '备库是否允许只读查询',
|
||||||
|
dependsOn: { key: 'enableReplication', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'synchronousCommit',
|
||||||
|
label: '同步提交模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'on - 等待 WAL 写入本地磁盘 (默认)', value: 'on' },
|
||||||
|
{ label: 'remote_write - 等待备库写入 WAL', value: 'remote_write' },
|
||||||
|
{ label: 'off - 异步提交,性能最高', value: 'off' },
|
||||||
|
],
|
||||||
|
default: 'on',
|
||||||
|
tip: '同步提交级别,影响数据安全与性能',
|
||||||
|
dependsOn: { key: 'enableReplication', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Autovacuum',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableAutovacuum',
|
||||||
|
label: '启用 Autovacuum',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '自动清理死元组并更新统计信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autovacuumMaxWorkers',
|
||||||
|
label: '最大工作进程数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
default: 3,
|
||||||
|
tip: '并行执行 autovacuum 的最大进程数',
|
||||||
|
dependsOn: { key: 'enableAutovacuum', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autovacuumNaptime',
|
||||||
|
label: '休眠间隔 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 86400,
|
||||||
|
default: 60,
|
||||||
|
tip: 'autovacuum 守护进程两次运行之间的休眠时间',
|
||||||
|
dependsOn: { key: 'enableAutovacuum', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autovacuumVacuumThreshold',
|
||||||
|
label: 'VACUUM 阈值',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 10000,
|
||||||
|
default: 50,
|
||||||
|
tip: '触发 VACUUM 的最小死元组数量',
|
||||||
|
dependsOn: { key: 'enableAutovacuum', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autovacuumAnalyzeThreshold',
|
||||||
|
label: 'ANALYZE 阈值',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 10000,
|
||||||
|
default: 50,
|
||||||
|
tip: '触发 ANALYZE 的最小变更行数',
|
||||||
|
dependsOn: { key: 'enableAutovacuum', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,5 +333,31 @@ export function generatePostgresqlConf(config) {
|
|||||||
lines.push(``)
|
lines.push(``)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 连接池
|
||||||
|
lines.push(`# ======================== 连接池 ========================`)
|
||||||
|
lines.push(`superuser_reserved_connections = ${config.superuserReservedConnections}`)
|
||||||
|
if (config.idleInTransactionSessionTimeout > 0) {
|
||||||
|
lines.push(`idle_in_transaction_session_timeout = ${config.idleInTransactionSessionTimeout}`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 流复制
|
||||||
|
if (config.enableReplication) {
|
||||||
|
lines.push(`# ======================== 流复制 ========================`)
|
||||||
|
lines.push(`hot_standby = ${config.hotStandby ? 'on' : 'off'}`)
|
||||||
|
lines.push(`synchronous_commit = ${config.synchronousCommit}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autovacuum
|
||||||
|
lines.push(`# ======================== Autovacuum ========================`)
|
||||||
|
lines.push(`autovacuum = ${config.enableAutovacuum ? 'on' : 'off'}`)
|
||||||
|
if (config.enableAutovacuum) {
|
||||||
|
lines.push(`autovacuum_max_workers = ${config.autovacuumMaxWorkers}`)
|
||||||
|
lines.push(`autovacuum_naptime = ${config.autovacuumNaptime}`)
|
||||||
|
lines.push(`autovacuum_vacuum_threshold = ${config.autovacuumVacuumThreshold}`)
|
||||||
|
lines.push(`autovacuum_analyze_threshold = ${config.autovacuumAnalyzeThreshold}`)
|
||||||
|
}
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,117 @@ export const redisSchema = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'ACL 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableAcl',
|
||||||
|
label: '启用 ACL',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '启用 Redis ACL 访问控制列表',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'aclFile',
|
||||||
|
label: 'ACL 文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/redis/users.acl',
|
||||||
|
tip: 'ACL 规则文件路径',
|
||||||
|
dependsOn: { key: 'enableAcl', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'defaultUserPassword',
|
||||||
|
label: '默认用户密码',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '留空表示无密码',
|
||||||
|
default: '',
|
||||||
|
tip: '设置 default 用户的密码',
|
||||||
|
dependsOn: { key: 'enableAcl', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enableRenameCommand',
|
||||||
|
label: '重命名危险命令',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '重命名 FLUSHALL/FLUSHDB/CONFIG 命令以增强安全性',
|
||||||
|
dependsOn: { key: 'enableAcl', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '慢日志',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'slowlogLogSlowerThan',
|
||||||
|
label: '慢日志阈值 (微秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 10000000,
|
||||||
|
default: 10000,
|
||||||
|
tip: '执行时间超过此值(微秒)的命令将被记录,0 表示记录所有',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'slowlogMaxLen',
|
||||||
|
label: '慢日志最大条数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10000,
|
||||||
|
default: 128,
|
||||||
|
tip: '慢日志队列最大长度,超出后旧记录被丢弃',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '延迟监控',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'latencyMonitorThreshold',
|
||||||
|
label: '延迟监控阈值 (毫秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 86400000,
|
||||||
|
default: 0,
|
||||||
|
tip: '0 = 禁用延迟监控,设置大于 0 的值启用(单位毫秒)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '客户端管理',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'maxmemorySamples',
|
||||||
|
label: '内存采样数量',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
default: 5,
|
||||||
|
tip: 'LRU/TTL 淘汰算法的采样数,越大越精确但越慢',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lazyfreeLazyEviction',
|
||||||
|
label: '惰性释放内存淘汰',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '内存淘汰时异步释放键,避免阻塞主线程',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lazyfreeLazyExpire',
|
||||||
|
label: '惰性释放过期键',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '过期键异步释放,避免阻塞主线程',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ioThreads',
|
||||||
|
label: 'IO 线程数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 8,
|
||||||
|
default: 1,
|
||||||
|
tip: '1 表示禁用多线程 IO,建议不超过 CPU 核心数',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +425,47 @@ export function generateRedisConf(config) {
|
|||||||
lines.push(`tcp-keepalive ${config.tcpKeepalive}`)
|
lines.push(`tcp-keepalive ${config.tcpKeepalive}`)
|
||||||
lines.push(`maxclients ${config.maxclients}`)
|
lines.push(`maxclients ${config.maxclients}`)
|
||||||
lines.push(`tcp-backlog 511`)
|
lines.push(`tcp-backlog 511`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// ACL 配置
|
||||||
|
if (config.enableAcl) {
|
||||||
|
lines.push(`################################## ACL 配置 ##################################`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`aclfile ${config.aclFile}`)
|
||||||
|
if (config.defaultUserPassword) {
|
||||||
|
lines.push(`# 默认用户密码已设置`)
|
||||||
|
lines.push(`acl setuser default >${config.defaultUserPassword} ~* +@all`)
|
||||||
|
}
|
||||||
|
if (config.enableRenameCommand) {
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`# 重命名危险命令`)
|
||||||
|
lines.push(`rename-command FLUSHALL ""`)
|
||||||
|
lines.push(`rename-command FLUSHDB ""`)
|
||||||
|
lines.push(`rename-command CONFIG ""`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 慢日志
|
||||||
|
lines.push(`################################## 慢日志 ##################################`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`slowlog-log-slower-than ${config.slowlogLogSlowerThan}`)
|
||||||
|
lines.push(`slowlog-max-len ${config.slowlogMaxLen}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 延迟监控
|
||||||
|
lines.push(`################################## 延迟监控 ##################################`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`latency-monitor-threshold ${config.latencyMonitorThreshold}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 客户端管理
|
||||||
|
lines.push(`################################## 客户端管理 ##################################`)
|
||||||
|
lines.push(``)
|
||||||
|
lines.push(`maxmemory-samples ${config.maxmemorySamples}`)
|
||||||
|
lines.push(`lazyfree-lazy-eviction ${config.lazyfreeLazyEviction ? 'yes' : 'no'}`)
|
||||||
|
lines.push(`lazyfree-lazy-expire ${config.lazyfreeLazyExpire ? 'yes' : 'no'}`)
|
||||||
|
lines.push(`io-threads ${config.ioThreads}`)
|
||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
export const rsyncSchema = {
|
||||||
|
id: 'rsync',
|
||||||
|
name: 'RSYNC',
|
||||||
|
icon: 'Files',
|
||||||
|
category: '系统服务',
|
||||||
|
description: '快速增量文件传输工具',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'rsyncd.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '全局配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'uid',
|
||||||
|
label: '用户 (uid)',
|
||||||
|
type: 'text',
|
||||||
|
default: 'root',
|
||||||
|
tip: '传输文件时使用的用户',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gid',
|
||||||
|
label: '用户组 (gid)',
|
||||||
|
type: 'text',
|
||||||
|
default: 'root',
|
||||||
|
tip: '传输文件时使用的用户组',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'useChroot',
|
||||||
|
label: '使用 chroot',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '是否限制在根目录下,提高安全性',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'port',
|
||||||
|
label: '监听端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 873,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxConnections',
|
||||||
|
label: '最大连接数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 1000,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'timeout',
|
||||||
|
label: '超时时间 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 3600,
|
||||||
|
default: 300,
|
||||||
|
tip: 'I/O 超时时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logFile',
|
||||||
|
label: '日志文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/log/rsyncd.log',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pidFile',
|
||||||
|
label: 'PID 文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/run/rsyncd.pid',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '模块配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableModule',
|
||||||
|
label: '启用共享模块',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '是否配置共享模块',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'moduleName',
|
||||||
|
label: '模块名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'backup',
|
||||||
|
dependsOn: { key: 'enableModule', value: true },
|
||||||
|
tip: '客户端访问时使用的模块名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'modulePath',
|
||||||
|
label: '模块路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/data/backup',
|
||||||
|
dependsOn: { key: 'enableModule', value: true },
|
||||||
|
tip: '共享目录的绝对路径',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'moduleComment',
|
||||||
|
label: '模块注释',
|
||||||
|
type: 'text',
|
||||||
|
default: 'Backup Area',
|
||||||
|
dependsOn: { key: 'enableModule', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'readOnly',
|
||||||
|
label: '只读模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
dependsOn: { key: 'enableModule', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'list',
|
||||||
|
label: '允许列出模块',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'enableModule', value: true },
|
||||||
|
tip: '客户端是否可以列出可用模块',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '认证与安全',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'authUsers',
|
||||||
|
label: '授权用户',
|
||||||
|
type: 'text',
|
||||||
|
default: 'backup_user',
|
||||||
|
tip: '多个用户用逗号分隔',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'secretsFile',
|
||||||
|
label: '密码文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/rsyncd.secrets',
|
||||||
|
tip: '格式: 用户名:密码,每行一个',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hostsAllow',
|
||||||
|
label: '允许的主机',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.0/24',
|
||||||
|
tip: 'IP 或网段,多个用逗号或空格分隔',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hostsDeny',
|
||||||
|
label: '拒绝的主机',
|
||||||
|
type: 'text',
|
||||||
|
default: '*',
|
||||||
|
tip: '* 表示拒绝所有未明确允许的主机',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRsyncConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# rsyncd.conf 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 全局配置
|
||||||
|
lines.push(`# ======================== 全局配置 ========================`)
|
||||||
|
lines.push(`uid = ${config.uid}`)
|
||||||
|
lines.push(`gid = ${config.gid}`)
|
||||||
|
lines.push(`use chroot = ${config.useChroot ? 'true' : 'false'}`)
|
||||||
|
lines.push(`port = ${config.port}`)
|
||||||
|
lines.push(`max connections = ${config.maxConnections}`)
|
||||||
|
lines.push(`timeout = ${config.timeout}`)
|
||||||
|
lines.push(`log file = ${config.logFile}`)
|
||||||
|
lines.push(`pid file = ${config.pidFile}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 模块配置
|
||||||
|
if (config.enableModule) {
|
||||||
|
lines.push(`# ======================== 模块配置 ========================`)
|
||||||
|
lines.push(`[${config.moduleName}]`)
|
||||||
|
lines.push(` comment = ${config.moduleComment}`)
|
||||||
|
lines.push(` path = ${config.modulePath}`)
|
||||||
|
lines.push(` read only = ${config.readOnly ? 'true' : 'false'}`)
|
||||||
|
lines.push(` list = ${config.list ? 'true' : 'false'}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证与安全
|
||||||
|
lines.push(`# ======================== 认证与安全 ========================`)
|
||||||
|
lines.push(`auth users = ${config.authUsers}`)
|
||||||
|
lines.push(`secrets file = ${config.secretsFile}`)
|
||||||
|
lines.push(`hosts allow = ${config.hostsAllow}`)
|
||||||
|
lines.push(`hosts deny = ${config.hostsDeny}`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
export const sambaSchema = {
|
||||||
|
id: 'samba',
|
||||||
|
name: 'SAMBA',
|
||||||
|
icon: 'Files',
|
||||||
|
category: '系统服务',
|
||||||
|
description: 'Samba 文件共享服务配置',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'smb.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '全局设置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'workgroup',
|
||||||
|
label: '工作组',
|
||||||
|
type: 'text',
|
||||||
|
default: 'WORKGROUP',
|
||||||
|
tip: 'Windows 工作组名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'serverString',
|
||||||
|
label: '服务器描述',
|
||||||
|
type: 'text',
|
||||||
|
default: 'Samba Server',
|
||||||
|
tip: '服务器的描述字符串',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'netbiosName',
|
||||||
|
label: 'NetBIOS 名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'smbserver',
|
||||||
|
tip: '网络中显示的计算机名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'securityType',
|
||||||
|
label: '安全模式',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'user - 用户级认证 (推荐)', value: 'user' },
|
||||||
|
{ label: 'share - 共享级认证', value: 'share' },
|
||||||
|
{ label: 'domain - 域认证', value: 'domain' },
|
||||||
|
],
|
||||||
|
default: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mapToGuest',
|
||||||
|
label: '访客映射',
|
||||||
|
type: 'text',
|
||||||
|
default: 'Bad User',
|
||||||
|
tip: '认证失败时映射为 guest 用户',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '共享目录',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableShare',
|
||||||
|
label: '启用共享',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '配置一个共享目录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shareName',
|
||||||
|
label: '共享名称',
|
||||||
|
type: 'text',
|
||||||
|
default: 'share',
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
tip: '网络访问路径中的共享名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sharePath',
|
||||||
|
label: '共享路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/data/samba',
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shareComment',
|
||||||
|
label: '共享注释',
|
||||||
|
type: 'text',
|
||||||
|
default: 'Shared Folder',
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'browseable',
|
||||||
|
label: '可浏览',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
tip: '是否在网络中可见',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'writable',
|
||||||
|
label: '可写',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'guestOk',
|
||||||
|
label: '允许匿名访问',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'validUsers',
|
||||||
|
label: '有效用户',
|
||||||
|
type: 'text',
|
||||||
|
default: 'user1 user2',
|
||||||
|
dependsOn: { key: 'enableShare', value: true },
|
||||||
|
tip: '允许访问的用户列表,空格分隔',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '权限掩码',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'createMask',
|
||||||
|
label: '文件创建掩码',
|
||||||
|
type: 'text',
|
||||||
|
default: '0664',
|
||||||
|
tip: '新建文件的权限掩码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'directoryMask',
|
||||||
|
label: '目录创建掩码',
|
||||||
|
type: 'text',
|
||||||
|
default: '0775',
|
||||||
|
tip: '新建目录的权限掩码',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '日志配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableLogging',
|
||||||
|
label: '启用日志',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logFile',
|
||||||
|
label: '日志文件',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/log/samba/log.%m',
|
||||||
|
dependsOn: { key: 'enableLogging', value: true },
|
||||||
|
tip: '%m 会被替换为客户端机器名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxLogSize',
|
||||||
|
label: '最大日志大小 (KB)',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 100000,
|
||||||
|
default: 5000,
|
||||||
|
dependsOn: { key: 'enableLogging', value: true },
|
||||||
|
tip: '0 表示不限制',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSambaConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# Samba 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Global section
|
||||||
|
lines.push(`# ======================== 全局设置 ========================`)
|
||||||
|
lines.push(`[global]`)
|
||||||
|
lines.push(` workgroup = ${config.workgroup}`)
|
||||||
|
lines.push(` server string = ${config.serverString}`)
|
||||||
|
lines.push(` netbios name = ${config.netbiosName}`)
|
||||||
|
lines.push(` security = ${config.securityType}`)
|
||||||
|
lines.push(` map to guest = ${config.mapToGuest}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
if (config.enableLogging) {
|
||||||
|
lines.push(` # 日志配置`)
|
||||||
|
lines.push(` log file = ${config.logFile}`)
|
||||||
|
if (config.maxLogSize > 0) {
|
||||||
|
lines.push(` max log size = ${config.maxLogSize}`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance tuning
|
||||||
|
lines.push(` # 性能优化`)
|
||||||
|
lines.push(` socket options = TCP_NODELAY IPTOS_LOWDELAY`)
|
||||||
|
lines.push(` load printers = no`)
|
||||||
|
lines.push(` printing = bsd`)
|
||||||
|
lines.push(` printcap name = /dev/null`)
|
||||||
|
lines.push(` disable spoolss = yes`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// Share section
|
||||||
|
if (config.enableShare) {
|
||||||
|
lines.push(`# ======================== 共享目录 ========================`)
|
||||||
|
lines.push(`[${config.shareName}]`)
|
||||||
|
lines.push(` comment = ${config.shareComment}`)
|
||||||
|
lines.push(` path = ${config.sharePath}`)
|
||||||
|
lines.push(` browseable = ${config.browseable ? 'yes' : 'no'}`)
|
||||||
|
lines.push(` writable = ${config.writable ? 'yes' : 'no'}`)
|
||||||
|
lines.push(` guest ok = ${config.guestOk ? 'yes' : 'no'}`)
|
||||||
|
|
||||||
|
if (!config.guestOk && config.validUsers) {
|
||||||
|
const users = config.validUsers.split(/\s+/).filter(Boolean).join(', ')
|
||||||
|
lines.push(` valid users = ${users}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` create mask = ${config.createMask}`)
|
||||||
|
lines.push(` directory mask = ${config.directoryMask}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tips
|
||||||
|
lines.push(`# ======================== 使用提示 ========================`)
|
||||||
|
lines.push(`# 1. 添加 Samba 用户: smbpasswd -a <username>`)
|
||||||
|
lines.push(`# 2. 确保目录存在: mkdir -p ${config.enableShare ? config.sharePath : '/data/samba'}`)
|
||||||
|
lines.push(`# 3. 设置权限: chmod 775 ${config.enableShare ? config.sharePath : '/data/samba'}`)
|
||||||
|
lines.push(`# 4. 重启服务: systemctl restart smb nmb`)
|
||||||
|
lines.push(`# 5. 测试配置: testparm`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
export const sersyncSchema = {
|
||||||
|
id: 'sersync',
|
||||||
|
name: 'Sersync',
|
||||||
|
icon: 'Refresh',
|
||||||
|
category: '系统服务',
|
||||||
|
description: '基于 inotify + rsync 的实时文件同步工具',
|
||||||
|
format: 'xml',
|
||||||
|
fileName: 'confxml.xml',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '基础配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'watchPath',
|
||||||
|
label: '监控目录',
|
||||||
|
type: 'text',
|
||||||
|
default: '/data/web',
|
||||||
|
tip: '需要实时同步的本地目录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'localDebug',
|
||||||
|
label: '本地调试模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '开启后仅记录日志不执行同步',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Rsync 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'rsyncCommand',
|
||||||
|
label: 'Rsync 命令路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/usr/bin/rsync',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rsyncArgs',
|
||||||
|
label: 'Rsync 参数',
|
||||||
|
type: 'text',
|
||||||
|
default: '-avz --delete',
|
||||||
|
tip: '常用的 rsync 同步参数',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rsyncUser',
|
||||||
|
label: 'Rsync 用户',
|
||||||
|
type: 'text',
|
||||||
|
default: 'root',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'remoteHosts',
|
||||||
|
label: '远程主机列表',
|
||||||
|
type: 'text',
|
||||||
|
default: '192.168.1.101::backup 192.168.1.102::backup',
|
||||||
|
tip: '格式: IP::模块名,多个用空格分隔',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'remotePassword',
|
||||||
|
label: '密码文件路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/rsyncd.secrets',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SSH 配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableSsh',
|
||||||
|
label: '启用 SSH 传输',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '使用 SSH 代替 rsync daemon 传输',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sshPort',
|
||||||
|
label: 'SSH 端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 22,
|
||||||
|
dependsOn: { key: 'enableSsh', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '监控配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableMonitor',
|
||||||
|
label: '开启 HTTP 监控',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '提供 HTTP 接口查看同步状态',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'monitorPort',
|
||||||
|
label: '监控端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 8008,
|
||||||
|
dependsOn: { key: 'enableMonitor', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'failLog',
|
||||||
|
label: '失败日志脚本',
|
||||||
|
type: 'text',
|
||||||
|
default: '/tmp/rsync_fail_log.sh',
|
||||||
|
tip: '同步失败时执行的脚本',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '定时任务与过滤',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableCron',
|
||||||
|
label: '启用定时同步',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '定时执行全量同步作为兜底',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cronInterval',
|
||||||
|
label: '定时同步间隔 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 60,
|
||||||
|
max: 86400,
|
||||||
|
default: 600,
|
||||||
|
dependsOn: { key: 'enableCron', value: true },
|
||||||
|
tip: '每隔多少秒执行一次全量同步',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'filterRule',
|
||||||
|
label: '启用过滤规则',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '过滤不需要同步的文件',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'filterExpression',
|
||||||
|
label: '过滤表达式',
|
||||||
|
type: 'text',
|
||||||
|
default: '*.tmp *.temp',
|
||||||
|
dependsOn: { key: 'filterRule', value: true },
|
||||||
|
tip: '匹配的文件不同步,空格分隔',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSersyncXml(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||||
|
lines.push(`<!-- Sersync 配置文件 - 由 ConfTemplate 生成 -->`)
|
||||||
|
lines.push(`<!-- 生成时间: ${new Date().toLocaleString('zh-CN')} -->`)
|
||||||
|
lines.push(`<head>`)
|
||||||
|
lines.push(` <host hostip="localhost" port="8008"></host>`)
|
||||||
|
lines.push(` <filter start="false">`)
|
||||||
|
|
||||||
|
if (config.filterRule && config.filterExpression) {
|
||||||
|
const expressions = config.filterExpression.split(/\s+/).filter(Boolean)
|
||||||
|
for (const expr of expressions) {
|
||||||
|
lines.push(` <exclude expression="${expr}"></exclude>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` </filter>`)
|
||||||
|
lines.push(` <inotify start="true">`)
|
||||||
|
lines.push(` <delete start="true"/>`)
|
||||||
|
lines.push(` <createFolder start="true"/>`)
|
||||||
|
lines.push(` <createFile start="false"/>`)
|
||||||
|
lines.push(` <closeWrite start="true"/>`)
|
||||||
|
lines.push(` <moveFrom start="true"/>`)
|
||||||
|
lines.push(` <moveTo start="true"/>`)
|
||||||
|
lines.push(` <attrib start="false"/>`)
|
||||||
|
lines.push(` <modify start="false"/>`)
|
||||||
|
lines.push(` </inotify>`)
|
||||||
|
lines.push(` <sersync>`)
|
||||||
|
lines.push(` <localpath watch="${config.watchPath}">`)
|
||||||
|
|
||||||
|
// 解析远程主机列表
|
||||||
|
const hosts = config.remoteHosts.split(/\s+/).filter(Boolean)
|
||||||
|
for (const host of hosts) {
|
||||||
|
if (config.enableSsh) {
|
||||||
|
const parts = host.split('::')
|
||||||
|
const ip = parts[0]
|
||||||
|
const remotePath = parts[1] || '/data/backup'
|
||||||
|
lines.push(` <remote ip="${ip}" name="${remotePath}"/>`)
|
||||||
|
} else {
|
||||||
|
const parts = host.split('::')
|
||||||
|
const ip = parts[0]
|
||||||
|
const module = parts[1] || 'backup'
|
||||||
|
lines.push(` <remote ip="${ip}" name="${module}"/>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` </localpath>`)
|
||||||
|
lines.push(` <rsync>`)
|
||||||
|
lines.push(` <commonParams params="${config.rsyncArgs}"/>`)
|
||||||
|
|
||||||
|
if (config.enableSsh) {
|
||||||
|
lines.push(` <ssh start="true" port="${config.sshPort}"/>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` <userDefinedPort start="false" port="874"/><!-- rsync daemon 端口 -->`)
|
||||||
|
lines.push(` <auth start="${config.rsyncUser ? 'true' : 'false'}" users="${config.rsyncUser}" passwordfile="${config.remotePassword}"/>`)
|
||||||
|
lines.push(` <userDefinedPort start="false" port="874"/>`)
|
||||||
|
lines.push(` <timeout start="true" time="1000"/><!-- timeout in ms -->`)
|
||||||
|
lines.push(` </rsync>`)
|
||||||
|
|
||||||
|
if (config.enableMonitor) {
|
||||||
|
lines.push(` <failLog path="${config.failLog}" timeToExecute="60"/><!-- 每60秒检查一次失败日志 -->`)
|
||||||
|
} else {
|
||||||
|
lines.push(` <failLog path="${config.failLog}" timeToExecute="60"/>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.enableCron) {
|
||||||
|
lines.push(` <crontab start="true" schedule="${Math.floor(config.cronInterval / 60)}"><!-- 分钟 -->`)
|
||||||
|
lines.push(` <crontabfilter start="false">`)
|
||||||
|
if (config.filterRule && config.filterExpression) {
|
||||||
|
const expressions = config.filterExpression.split(/\s+/).filter(Boolean)
|
||||||
|
for (const expr of expressions) {
|
||||||
|
lines.push(` <exclude expression="${expr}"></exclude>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(` </crontabfilter>`)
|
||||||
|
lines.push(` </crontab>`)
|
||||||
|
} else {
|
||||||
|
lines.push(` <crontab start="false" schedule="600">`)
|
||||||
|
lines.push(` <crontabfilter start="false">`)
|
||||||
|
lines.push(` </crontabfilter>`)
|
||||||
|
lines.push(` </crontab>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` <plugin start="false" name="command"/>`)
|
||||||
|
lines.push(` </sersync>`)
|
||||||
|
lines.push(` <plugin name="command">`)
|
||||||
|
lines.push(` <param prefix="/bin/sh" suffix="" ignoreError="true"/>`)
|
||||||
|
lines.push(` <filter start="false">`)
|
||||||
|
lines.push(` <include expression="(.*)\.php"/>`)
|
||||||
|
lines.push(` <include expression="(.*)\.sh"/>`)
|
||||||
|
lines.push(` </filter>`)
|
||||||
|
lines.push(` </plugin>`)
|
||||||
|
lines.push(` <plugin name="socket">`)
|
||||||
|
lines.push(` <localpath watch="/opt/tongbu">`)
|
||||||
|
lines.push(` <deshost ip="192.168.1.101" port="8009"/>`)
|
||||||
|
lines.push(` </localpath>`)
|
||||||
|
lines.push(` </plugin>`)
|
||||||
|
lines.push(`</head>`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
export const subversionSchema = {
|
||||||
|
id: 'subversion',
|
||||||
|
name: 'Subversion (SVN)',
|
||||||
|
icon: 'Files',
|
||||||
|
category: '系统服务',
|
||||||
|
description: '集中式版本控制系统',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'svnserve.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '访问控制',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'anonAccess',
|
||||||
|
label: '匿名访问权限',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'none - 禁止匿名访问 (推荐)', value: 'none' },
|
||||||
|
{ label: 'read - 匿名用户只读', value: 'read' },
|
||||||
|
{ label: 'write - 匿名用户可写', value: 'write' },
|
||||||
|
],
|
||||||
|
default: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authAccess',
|
||||||
|
label: '认证用户权限',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'write - 认证用户可写 (推荐)', value: 'write' },
|
||||||
|
{ label: 'read - 认证用户只读', value: 'read' },
|
||||||
|
],
|
||||||
|
default: 'write',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'passwordDb',
|
||||||
|
label: '密码数据库文件',
|
||||||
|
type: 'text',
|
||||||
|
default: 'passwd',
|
||||||
|
tip: '相对于 conf 目录的路径',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'authzDb',
|
||||||
|
label: '权限数据库文件',
|
||||||
|
type: 'text',
|
||||||
|
default: 'authz',
|
||||||
|
tip: '相对于 conf 目录的路径',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'realm',
|
||||||
|
label: '认证域',
|
||||||
|
type: 'text',
|
||||||
|
default: 'My Repository',
|
||||||
|
tip: '认证域名称,用于区分不同的仓库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'listenPort',
|
||||||
|
label: '监听端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 3690,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'SASL 认证',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableSasl',
|
||||||
|
label: '启用 SASL 认证',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '使用 SASL 框架进行认证,支持更高级的加密',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'saslRealm',
|
||||||
|
label: 'SASL 域',
|
||||||
|
type: 'text',
|
||||||
|
default: '',
|
||||||
|
dependsOn: { key: 'enableSasl', value: true },
|
||||||
|
tip: 'SASL 认证域,留空使用默认',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'minEncryption',
|
||||||
|
label: '最小加密强度',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 256,
|
||||||
|
default: 0,
|
||||||
|
dependsOn: { key: 'enableSasl', value: true },
|
||||||
|
tip: '0=不加密, 1=仅完整性, 128=128位, 256=256位',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxEncryption',
|
||||||
|
label: '最大加密强度',
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
max: 256,
|
||||||
|
default: 256,
|
||||||
|
dependsOn: { key: 'enableSasl', value: true },
|
||||||
|
tip: '0=不加密, 1=仅完整性, 128=128位, 256=256位',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '钩子脚本',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableHooks',
|
||||||
|
label: '启用钩子脚本',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '在特定 SVN 事件时执行自定义脚本',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hookType',
|
||||||
|
label: '钩子类型',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'post-commit - 提交后执行 (推荐)', value: 'post-commit' },
|
||||||
|
{ label: 'pre-commit - 提交前执行', value: 'pre-commit' },
|
||||||
|
],
|
||||||
|
default: 'post-commit',
|
||||||
|
dependsOn: { key: 'enableHooks', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hookScript',
|
||||||
|
label: '钩子脚本内容',
|
||||||
|
type: 'text',
|
||||||
|
default: '#!/bin/sh\n/usr/bin/svn update /var/www/html',
|
||||||
|
dependsOn: { key: 'enableHooks', value: true },
|
||||||
|
tip: '钩子脚本的完整内容',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSubversionConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# svnserve.conf 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(`#`)
|
||||||
|
lines.push(`# 将此文件放置在仓库的 conf/ 目录下`)
|
||||||
|
lines.push(`# 启动命令: svnserve -d -r /path/to/repos --listen-port ${config.listenPort}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 通用配置
|
||||||
|
lines.push(`[general]`)
|
||||||
|
|
||||||
|
// 匿名访问
|
||||||
|
lines.push(`# 匿名访问权限`)
|
||||||
|
lines.push(`anon-access = ${config.anonAccess}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 认证用户访问
|
||||||
|
lines.push(`# 认证用户访问权限`)
|
||||||
|
lines.push(`auth-access = ${config.authAccess}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 密码数据库
|
||||||
|
lines.push(`# 密码数据库`)
|
||||||
|
lines.push(`password-db = ${config.passwordDb}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 权限数据库
|
||||||
|
lines.push(`# 权限数据库`)
|
||||||
|
lines.push(`authz-db = ${config.authzDb}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 认证域
|
||||||
|
lines.push(`# 认证域`)
|
||||||
|
lines.push(`realm = ${config.realm}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// SASL 认证
|
||||||
|
if (config.enableSasl) {
|
||||||
|
lines.push(`[sasl]`)
|
||||||
|
lines.push(`# 启用 SASL 认证`)
|
||||||
|
lines.push(`use-sasl = true`)
|
||||||
|
|
||||||
|
if (config.saslRealm) {
|
||||||
|
lines.push(`sasl-realm = ${config.saslRealm}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`min-encryption = ${config.minEncryption}`)
|
||||||
|
lines.push(`max-encryption = ${config.maxEncryption}`)
|
||||||
|
lines.push(``)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 钩子脚本说明
|
||||||
|
if (config.enableHooks) {
|
||||||
|
lines.push(`# ======================== 钩子脚本 ========================`)
|
||||||
|
lines.push(`# 以下为 ${config.hookType} 钩子脚本`)
|
||||||
|
lines.push(`# 请将此内容保存为: <仓库路径>/hooks/${config.hookType}`)
|
||||||
|
lines.push(`# 并确保有执行权限: chmod +x <仓库路径>/hooks/${config.hookType}`)
|
||||||
|
lines.push(`#`)
|
||||||
|
lines.push(`# ---- 脚本内容开始 ----`)
|
||||||
|
lines.push(config.hookScript)
|
||||||
|
lines.push(`# ---- 脚本内容结束 ----`)
|
||||||
|
lines.push(`#`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// passwd 文件示例
|
||||||
|
lines.push(`# ======================== 附录 ========================`)
|
||||||
|
lines.push(`# passwd 文件示例 (${config.passwordDb}):`)
|
||||||
|
lines.push(`# [users]`)
|
||||||
|
lines.push(`# admin = admin123`)
|
||||||
|
lines.push(`# developer = dev123`)
|
||||||
|
lines.push(`#`)
|
||||||
|
lines.push(`# authz 文件示例 (${config.authzDb}):`)
|
||||||
|
lines.push(`# [groups]`)
|
||||||
|
lines.push(`# admin = admin`)
|
||||||
|
lines.push(`# developers = developer`)
|
||||||
|
lines.push(`#`)
|
||||||
|
lines.push(`# [/]`)
|
||||||
|
lines.push(`# @admin = rw`)
|
||||||
|
lines.push(`# * = ${config.anonAccess === 'none' ? '' : config.anonAccess}`)
|
||||||
|
lines.push(`#`)
|
||||||
|
lines.push(`# [/trunk]`)
|
||||||
|
lines.push(`# @developers = rw`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
export const vsftpdSchema = {
|
||||||
|
id: 'vsftpd',
|
||||||
|
name: 'vsftpd',
|
||||||
|
icon: 'Upload',
|
||||||
|
category: '系统服务',
|
||||||
|
description: '非常安全的 FTP 守护进程',
|
||||||
|
format: 'conf',
|
||||||
|
fileName: 'vsftpd.conf',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: '基础配置',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'listenPort',
|
||||||
|
label: '监听端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65535,
|
||||||
|
default: 21,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'anonymousEnable',
|
||||||
|
label: '允许匿名访问',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '生产环境建议关闭',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'localEnable',
|
||||||
|
label: '允许本地用户登录',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'writeEnable',
|
||||||
|
label: '允许写操作',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: 'FTP 写命令权限 (STOR, DELE 等)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'localUmask',
|
||||||
|
label: '本地用户 umask',
|
||||||
|
type: 'text',
|
||||||
|
default: '022',
|
||||||
|
tip: '新建文件/目录的权限掩码',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户限制',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'chrootLocalUser',
|
||||||
|
label: '限制用户在家目录',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: 'chroot 将用户限制在其主目录',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'allowWriteableChroot',
|
||||||
|
label: '允许可写 chroot',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'chrootLocalUser', value: true },
|
||||||
|
tip: 'chroot 目录可写时需要开启',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxClients',
|
||||||
|
label: '最大客户端数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 10000,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'maxPerIp',
|
||||||
|
label: '单 IP 最大连接数',
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'idleSessionTimeout',
|
||||||
|
label: '空闲会话超时 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 30,
|
||||||
|
max: 3600,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dataConnectionTimeout',
|
||||||
|
label: '数据连接超时 (秒)',
|
||||||
|
type: 'number',
|
||||||
|
min: 30,
|
||||||
|
max: 3600,
|
||||||
|
default: 120,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '被动模式',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'pasvEnable',
|
||||||
|
label: '启用被动模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
tip: '防火墙/NAT 后的客户端需要被动模式',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pasvMinPort',
|
||||||
|
label: '被动模式最小端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1024,
|
||||||
|
max: 65535,
|
||||||
|
default: 30000,
|
||||||
|
dependsOn: { key: 'pasvEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pasvMaxPort',
|
||||||
|
label: '被动模式最大端口',
|
||||||
|
type: 'number',
|
||||||
|
min: 1024,
|
||||||
|
max: 65535,
|
||||||
|
default: 30100,
|
||||||
|
dependsOn: { key: 'pasvEnable', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'TLS/SSL 安全',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'enableTls',
|
||||||
|
label: '启用 TLS/SSL',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '通过 FTPS 加密传输',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rsaCertFile',
|
||||||
|
label: 'SSL 证书路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/ssl/certs/vsftpd.pem',
|
||||||
|
dependsOn: { key: 'enableTls', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rsaPrivateKeyFile',
|
||||||
|
label: 'SSL 私钥路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/etc/ssl/private/vsftpd.key',
|
||||||
|
dependsOn: { key: 'enableTls', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sslTlsv1',
|
||||||
|
label: '启用 TLSv1',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
dependsOn: { key: 'enableTls', value: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户列表与日志',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
key: 'userlistEnable',
|
||||||
|
label: '启用用户列表',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userlistDeny',
|
||||||
|
label: '用户列表为黑名单',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
dependsOn: { key: 'userlistEnable', value: true },
|
||||||
|
tip: 'false=白名单 (允许列表中用户), true=黑名单 (拒绝列表中用户)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logEnable',
|
||||||
|
label: '启用日志',
|
||||||
|
type: 'switch',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'xferlogFile',
|
||||||
|
label: '传输日志路径',
|
||||||
|
type: 'text',
|
||||||
|
default: '/var/log/vsftpd.log',
|
||||||
|
dependsOn: { key: 'logEnable', value: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'asciiUploadEnable',
|
||||||
|
label: 'ASCII 上传模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '允许 ASCII 模式上传',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'asciiDownloadEnable',
|
||||||
|
label: 'ASCII 下载模式',
|
||||||
|
type: 'switch',
|
||||||
|
default: false,
|
||||||
|
tip: '允许 ASCII 模式下载',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ftpdBanner',
|
||||||
|
label: '欢迎横幅',
|
||||||
|
type: 'text',
|
||||||
|
default: 'Welcome to FTP service',
|
||||||
|
tip: '用户登录时显示的欢迎信息',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateVsftpdConf(config) {
|
||||||
|
const lines = []
|
||||||
|
lines.push(`# vsftpd.conf 配置文件 - 由 ConfTemplate 生成`)
|
||||||
|
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 基础配置
|
||||||
|
lines.push(`# ======================== 基础配置 ========================`)
|
||||||
|
lines.push(`listen=YES`)
|
||||||
|
lines.push(`listen_port=${config.listenPort}`)
|
||||||
|
lines.push(`anonymous_enable=${config.anonymousEnable ? 'YES' : 'NO'}`)
|
||||||
|
lines.push(`local_enable=${config.localEnable ? 'YES' : 'NO'}`)
|
||||||
|
lines.push(`write_enable=${config.writeEnable ? 'YES' : 'NO'}`)
|
||||||
|
lines.push(`local_umask=${config.localUmask}`)
|
||||||
|
lines.push(`dirmessage_enable=YES`)
|
||||||
|
lines.push(`use_localtime=YES`)
|
||||||
|
lines.push(`xferlog_enable=YES`)
|
||||||
|
lines.push(`connect_from_port_20=YES`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 用户限制
|
||||||
|
lines.push(`# ======================== 用户限制 ========================`)
|
||||||
|
lines.push(`chroot_local_user=${config.chrootLocalUser ? 'YES' : 'NO'}`)
|
||||||
|
if (config.chrootLocalUser) {
|
||||||
|
lines.push(`allow_writeable_chroot=${config.allowWriteableChroot ? 'YES' : 'NO'}`)
|
||||||
|
}
|
||||||
|
lines.push(`max_clients=${config.maxClients}`)
|
||||||
|
lines.push(`max_per_ip=${config.maxPerIp}`)
|
||||||
|
lines.push(`idle_session_timeout=${config.idleSessionTimeout}`)
|
||||||
|
lines.push(`data_connection_timeout=${config.dataConnectionTimeout}`)
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 被动模式
|
||||||
|
lines.push(`# ======================== 被动模式 ========================`)
|
||||||
|
lines.push(`pasv_enable=${config.pasvEnable ? 'YES' : 'NO'}`)
|
||||||
|
if (config.pasvEnable) {
|
||||||
|
lines.push(`pasv_min_port=${config.pasvMinPort}`)
|
||||||
|
lines.push(`pasv_max_port=${config.pasvMaxPort}`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// TLS/SSL 安全
|
||||||
|
lines.push(`# ======================== TLS/SSL 安全 ========================`)
|
||||||
|
if (config.enableTls) {
|
||||||
|
lines.push(`ssl_enable=YES`)
|
||||||
|
lines.push(`rsa_cert_file=${config.rsaCertFile}`)
|
||||||
|
lines.push(`rsa_private_key_file=${config.rsaPrivateKeyFile}`)
|
||||||
|
lines.push(`ssl_tlsv1=${config.sslTlsv1 ? 'YES' : 'NO'}`)
|
||||||
|
lines.push(`ssl_sslv2=NO`)
|
||||||
|
lines.push(`ssl_sslv3=NO`)
|
||||||
|
lines.push(`require_ssl_reuse=NO`)
|
||||||
|
lines.push(`ssl_ciphers=HIGH`)
|
||||||
|
} else {
|
||||||
|
lines.push(`ssl_enable=NO`)
|
||||||
|
}
|
||||||
|
lines.push(``)
|
||||||
|
|
||||||
|
// 用户列表与日志
|
||||||
|
lines.push(`# ======================== 用户列表与日志 ========================`)
|
||||||
|
lines.push(`userlist_enable=${config.userlistEnable ? 'YES' : 'NO'}`)
|
||||||
|
if (config.userlistEnable) {
|
||||||
|
lines.push(`userlist_deny=${config.userlistDeny ? 'YES' : 'NO'}`)
|
||||||
|
}
|
||||||
|
lines.push(`log_enable=${config.logEnable ? 'YES' : 'NO'}`)
|
||||||
|
if (config.logEnable) {
|
||||||
|
lines.push(`xferlog_file=${config.xferlogFile}`)
|
||||||
|
lines.push(`xferlog_std_format=YES`)
|
||||||
|
}
|
||||||
|
lines.push(`ascii_upload_enable=${config.asciiUploadEnable ? 'YES' : 'NO'}`)
|
||||||
|
lines.push(`ascii_download_enable=${config.asciiDownloadEnable ? 'YES' : 'NO'}`)
|
||||||
|
lines.push(`ftpd_banner=${config.ftpdBanner}`)
|
||||||
|
lines.push(`pam_service_name=vsftpd`)
|
||||||
|
lines.push(`tcp_wrappers=YES`)
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user