Files
dify-ee-helm-chart-values-g…/generate-values-prd.py
T
Petrus Han d5ce3ac7ef Initial commit: Dify Helm Chart Values Generator
- Add interactive Python script for generating values-prd.yaml
- Add comprehensive documentation in docs/
- Add pyproject.toml and requirements.txt
- Add .gitignore to exclude sensitive files
- Add README.md with project overview and usage instructions
2025-11-20 18:19:37 +08:00

1245 lines
52 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Dify Helm Chart Values Generator
交互式生成 values-prd.yaml 配置文件
模块划分和联动关系:
1. 全局配置模块 (global) - 影响所有服务
2. 基础设施模块 - 数据库、存储、缓存(互斥选择)
3. 服务模块 - 应用服务配置
4. 网络模块 - Ingress配置
5. 邮件模块 - 邮件服务配置
"""
import yaml
import os
import subprocess
import sys
import re
import shutil
from typing import Dict, Any, Optional
from copy import deepcopy
class Colors:
"""终端颜色"""
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def print_header(text: str):
"""打印标题"""
print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}")
print(f"{Colors.HEADER}{Colors.BOLD}{text:^60}{Colors.ENDC}")
print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}\n")
def print_section(text: str):
"""打印章节"""
print(f"\n{Colors.OKCYAN}{Colors.BOLD}>>> {text}{Colors.ENDC}")
def print_info(text: str):
"""打印信息"""
print(f"{Colors.OKBLUE} {text}{Colors.ENDC}")
def print_success(text: str):
"""打印成功信息"""
print(f"{Colors.OKGREEN}{text}{Colors.ENDC}")
def print_warning(text: str):
"""打印警告"""
print(f"{Colors.WARNING}{text}{Colors.ENDC}")
def print_error(text: str):
"""打印错误"""
print(f"{Colors.FAIL}{text}{Colors.ENDC}")
def prompt(prompt_text: str, default: Optional[str] = None, required: bool = True) -> str:
"""提示用户输入"""
if default:
prompt_str = f"{Colors.BOLD}{prompt_text}{Colors.ENDC} [{default}]: "
else:
prompt_str = f"{Colors.BOLD}{prompt_text}{Colors.ENDC}: "
while True:
value = input(prompt_str).strip()
if value:
return value
elif default:
return default
elif not required:
return ""
else:
print_error("此字段为必填项,请重新输入")
def prompt_yes_no(prompt_text: str, default: bool = True) -> bool:
"""提示是/否选择"""
default_str = "Y/n" if default else "y/N"
prompt_str = f"{Colors.BOLD}{prompt_text}{Colors.ENDC} [{default_str}]: "
while True:
value = input(prompt_str).strip().lower()
if not value:
return default
if value in ['y', 'yes']:
return True
elif value in ['n', 'no']:
return False
else:
print_error("请输入 y 或 n")
def prompt_choice(prompt_text: str, choices: list, default: Optional[str] = None) -> str:
"""提示选择"""
print(f"\n{Colors.BOLD}{prompt_text}{Colors.ENDC}")
for i, choice in enumerate(choices, 1):
marker = " [默认]" if choice == default else ""
print(f" {i}. {choice}{marker}")
while True:
if default:
prompt_str = f"请选择 [1-{len(choices)}] (默认: {default}): "
else:
prompt_str = f"请选择 [1-{len(choices)}]: "
value = input(prompt_str).strip()
if not value and default:
return default
try:
idx = int(value) - 1
if 0 <= idx < len(choices):
return choices[idx]
except ValueError:
pass
print_error(f"请输入 1-{len(choices)} 之间的数字")
def generate_secret(length: int = 42) -> str:
"""生成密钥"""
try:
result = subprocess.run(
['openssl', 'rand', '-base64', str(length)],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError):
print_warning("无法使用 openssl 生成密钥,将使用随机字符串")
import secrets
return secrets.token_urlsafe(length)
class ValuesGenerator:
"""Values 生成器"""
def __init__(self, source_file: str):
"""初始化"""
self.source_file = source_file
self.values = {}
self.yaml_data = None # ruamel.yaml 的数据对象(保留注释和格式)
self.load_template()
def load_template(self):
"""加载模板文件"""
try:
# 尝试使用 ruamel.yaml(推荐方式)
try:
from ruamel.yaml import YAML
yaml_loader = YAML()
yaml_loader.preserve_quotes = True
yaml_loader.width = 120
yaml_loader.indent(mapping=2, sequence=4, offset=2)
# 读取原始文件(保留注释和格式)
with open(self.source_file, 'r', encoding='utf-8') as f:
self.yaml_data = yaml_loader.load(f)
# 同时加载为标准字典用于配置逻辑
with open(self.source_file, 'r', encoding='utf-8') as f:
self.values = yaml.safe_load(f)
print_success(f"已加载模板文件: {self.source_file} (使用 ruamel.yaml,保留注释和格式)")
except ImportError:
# 回退到标准 yaml
with open(self.source_file, 'r', encoding='utf-8') as f:
self.values = yaml.safe_load(f)
print_success(f"已加载模板文件: {self.source_file}")
print_warning("ruamel.yaml 未安装,建议安装以获得更好的格式保留: pip install ruamel.yaml")
except Exception as e:
print_error(f"加载模板文件失败: {e}")
sys.exit(1)
def set_value(self, key_path: str, value: Any):
"""
设置值(同时更新 yaml_data 和 values
Args:
key_path: 键路径,如 'global.appSecretKey'
value: 新值
"""
keys = key_path.split('.')
# 更新标准字典
current = self.values
for key in keys[:-1]:
if key not in current:
current[key] = {}
current = current[key]
current[keys[-1]] = value
# 更新 ruamel.yaml 数据对象
if self.yaml_data is not None:
try:
from ruamel.yaml.comments import CommentedMap
current = self.yaml_data
for key in keys[:-1]:
if key not in current:
current[key] = CommentedMap()
current = current[key]
current[keys[-1]] = value
except Exception:
# 如果更新失败,至少标准字典已更新
pass
def save(self, output_file: str):
"""
保存到文件 - 优先使用 ruamel.yaml 保留注释和格式,否则使用标准 yaml
"""
try:
# 尝试使用 ruamel.yaml(推荐方式)
try:
from ruamel.yaml import YAML
yaml_loader = YAML()
yaml_loader.preserve_quotes = True
yaml_loader.width = 120
yaml_loader.indent(mapping=2, sequence=4, offset=2)
yaml_loader.default_flow_style = False
# 重新加载原始文件(保留注释和格式)
with open(self.source_file, 'r', encoding='utf-8') as f:
data = yaml_loader.load(f)
# 递归更新值(将 self.values 的更改应用到 data
self._update_dict_recursive(data, self.values)
# 保存
with open(output_file, 'w', encoding='utf-8') as f:
yaml_loader.dump(data, f)
print_success(f"配置已保存到: {output_file}")
print_info("✓ 已保留原始格式、注释和引号(使用 ruamel.yaml")
except ImportError:
# 回退到标准 yaml(会丢失注释和格式)
print_warning("ruamel.yaml 未安装,使用标准 yaml 库")
print_info("建议安装以获得更好的格式保留: pip install ruamel.yaml")
self._save_with_standard_yaml(output_file)
except Exception as e:
print_warning(f"使用 ruamel.yaml 保存失败: {e}")
print_info("回退到标准 yaml 库")
self._save_with_standard_yaml(output_file)
except Exception as e:
print_error(f"保存文件失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
def _update_dict_recursive(self, target: dict, source: dict):
"""递归更新字典,保留 ruamel.yaml 的格式和注释"""
for key, value in source.items():
if key in target:
if isinstance(value, dict) and isinstance(target[key], dict):
self._update_dict_recursive(target[key], value)
else:
target[key] = value
def _save_with_standard_yaml(self, output_file: str):
"""使用标准 yaml 库保存(会丢失注释和格式)"""
print_warning("使用标准 yaml 库,可能丢失注释和格式")
print_info("建议安装 ruamel.yaml: pip install ruamel.yaml")
with open(output_file, 'w', encoding='utf-8') as f:
yaml.dump(self.values, f, default_flow_style=False,
allow_unicode=True, sort_keys=False,
width=120, indent=2)
print_success(f"配置已保存到: {output_file}")
def _update_dict_recursive(self, target: dict, source: dict):
"""递归更新字典,保留 ruamel.yaml 的格式和注释"""
for key, value in source.items():
if key in target:
if isinstance(value, dict) and isinstance(target[key], dict):
self._update_dict_recursive(target[key], value)
else:
target[key] = value
def _save_with_text_replacement(self, output_file: str):
"""使用文本替换方式保存,保留注释和格式"""
# 先复制模板文件
content = self.template_content
# 获取原始值用于比较
with open(self.source_file, 'r', encoding='utf-8') as f:
original_data = yaml.safe_load(f)
# 找出所有需要更新的值
def find_changes(new_dict: dict, old_dict: dict, path: str = ""):
changes = []
for k, v in new_dict.items():
current_path = f"{path}.{k}" if path else k
if k not in old_dict:
changes.append((current_path, v))
elif isinstance(v, dict) and isinstance(old_dict[k], dict):
changes.extend(find_changes(v, old_dict[k], current_path))
elif v != old_dict[k]:
changes.append((current_path, v))
return changes
changes = find_changes(self.values, original_data)
# 对每个变更进行文本替换
for path, new_value in changes:
keys = path.split('.')
# 构建正则表达式匹配模式
if len(keys) == 1:
# 简单键:匹配 "key: value" 格式
# 需要处理多行值(如注释、多行字符串)
pattern = rf'^(\s*){re.escape(keys[0])}\s*:(.*?)(?=\n\s*\w+\s*:|\n\s*$|\Z)'
def replace_func(match):
indent = match.group(1)
old_value_part = match.group(2)
# 生成新值
if new_value is None:
return f"{indent}{keys[0]}:"
elif isinstance(new_value, str):
# 检查原值是否有引号
old_stripped = old_value_part.strip()
has_quotes = old_stripped.startswith('"') or old_stripped.startswith("'")
# 检查是否需要引号
needs_quotes = (has_quotes or new_value == '' or
':' in new_value or new_value.startswith('*') or
new_value.startswith('#') or ' ' in new_value)
if needs_quotes:
return f"{indent}{keys[0]}: \"{new_value}\""
else:
return f"{indent}{keys[0]}: {new_value}"
elif isinstance(new_value, bool):
return f"{indent}{keys[0]}: {str(new_value).lower()}"
elif isinstance(new_value, (int, float)):
return f"{indent}{keys[0]}: {new_value}"
elif isinstance(new_value, list):
if len(new_value) == 0:
return f"{indent}{keys[0]}: []"
else:
result = f"{indent}{keys[0]}:\n"
for item in new_value:
if isinstance(item, dict):
for k, v in item.items():
result += f"{indent} {k}: {v}\n"
else:
result += f"{indent} - {item}\n"
return result.rstrip()
else:
return f"{indent}{keys[0]}: {new_value}"
# 使用多行模式匹配
content = re.sub(pattern, replace_func, content, flags=re.MULTILINE | re.DOTALL)
# 写入文件
with open(output_file, 'w', encoding='utf-8') as f:
f.write(content)
print_success(f"配置已保存到: {output_file}")
print_warning("⚠ 文本替换方式可能无法完美保留所有格式,建议安装 ruamel.yaml")
# ==================== 模块 1: 全局配置 ====================
def configure_global(self):
"""配置全局设置"""
print_header("模块 1: 全局配置 (Global Configuration)")
print_info("全局配置影响所有服务的运行")
# Secret Keys - 所有密钥都按注释自动生成
print_section("密钥配置")
print_info("appSecretKey 用于安全签名会话cookie和加密数据库敏感信息")
print_info("将使用 openssl rand -base64 42 自动生成")
self.values['global']['appSecretKey'] = generate_secret(42)
print_success(f"已生成 appSecretKey: {self.values['global']['appSecretKey'][:20]}...")
print_info("innerApiKey 用于内部API调用的密钥")
print_info("将使用 openssl rand -base64 42 自动生成")
self.values['global']['innerApiKey'] = generate_secret(42)
print_success(f"已生成 innerApiKey: {self.values['global']['innerApiKey'][:20]}...")
# 域名配置
print_section("域名配置")
print_info("如果为空,将使用相同域名")
self.values['global']['consoleApiDomain'] = prompt(
"Console API 域名",
default="console.dify.local",
required=False
)
self.values['global']['consoleWebDomain'] = prompt(
"Console Web 域名",
default="console.dify.local",
required=False
)
self.values['global']['serviceApiDomain'] = prompt(
"Service API 域名",
default="api.dify.local",
required=False
)
self.values['global']['appApiDomain'] = prompt(
"WebApp API 后端域名",
default="app.dify.local",
required=False
)
self.values['global']['appWebDomain'] = prompt(
"WebApp 域名",
default="app.dify.local",
required=False
)
self.values['global']['filesDomain'] = prompt(
"文件预览/下载域名",
default="files.dify.local",
required=False
)
self.values['global']['enterpriseDomain'] = prompt(
"Enterprise 服务域名",
default="enterprise.dify.local",
required=False
)
# 数据库迁移
self.values['global']['dbMigrationEnabled'] = prompt_yes_no(
"是否启用数据库迁移?",
default=True
)
# RAG配置
print_section("RAG 配置")
rag_etl_type = prompt_choice(
"RAG ETL 类型",
["dify", "Unstructured"],
default="dify"
)
self.values['global']['rag']['etlType'] = rag_etl_type
# 联动关系: 如果选择 dify,则关闭 unstructured 模块
if rag_etl_type == "dify":
self.values['unstructured']['enabled'] = False
print_info("已自动关闭 unstructured 模块(RAG ETL 类型为 dify")
else:
self.values['unstructured']['enabled'] = True
print_info("已自动启用 unstructured 模块(RAG ETL 类型为 Unstructured")
# 关键词数据源类型配置 - 添加详细说明
print_section("关键词数据源类型配置")
print_info("=" * 60)
print_info("重要说明:关键词数据源类型")
print_info("=" * 60)
print_info("此配置决定 RAG 关键词检索(Keyword Search)时的关键词存储位置。")
print_info("")
print_info("选项说明:")
print_info(" • object_storage: 将关键词存储在对象存储中(如 MinIO、S3)")
print_info(" - 适合大规模关键词存储")
print_info(" - 需要配置对象存储服务")
print_info("")
print_info(" • database: 将关键词存储在数据库中")
print_info(" - 适合中小规模关键词存储")
print_info(" - 使用已配置的 PostgreSQL 数据库")
print_info("=" * 60)
print_info("")
self.values['global']['rag']['keywordDataSourceType'] = prompt_choice(
"请选择关键词数据源类型",
["object_storage", "database"],
default="object_storage"
)
top_k = prompt("RAG Top-K 最大值", default="10", required=False)
try:
self.values['global']['rag']['topKMaxValue'] = int(top_k)
except ValueError:
print_warning("无效的数字,使用默认值 10")
seg_tokens = prompt("文档分块最大token长度", default="4000", required=False)
try:
self.values['global']['rag']['indexingMaxSegmentationTokensLength'] = int(seg_tokens)
except ValueError:
print_warning("无效的数字,使用默认值 4000")
# ==================== 模块 2: 基础设施配置 ====================
def configure_infrastructure(self):
"""配置基础设施"""
print_header("模块 2: 基础设施配置 (Infrastructure)")
# PostgreSQL
print_section("PostgreSQL 数据库配置")
print_info("=" * 60)
print_info("网络地址说明(重要)")
print_info("=" * 60)
print_info("如果在 kind 集群中运行,访问集群外(同一主机)的服务:")
print_info(" • 使用 'host.docker.internal' 访问宿主机服务")
print_info(" • 或使用宿主机 IP 地址")
print_info(" • 或使用 'localhost'(如果配置了端口映射)")
print_info("")
print_info("如果服务在集群内:")
print_info(" • 使用服务名,例如: postgresql.default.svc.cluster.local")
print_info(" • 或使用短服务名,例如: postgresql")
print_info("=" * 60)
print_info("")
# 默认使用外部 PostgreSQL(企业版推荐)
use_external_postgres = True
print_info("默认使用外部 PostgreSQL(企业版推荐配置)")
if use_external_postgres:
self.values['externalPostgres']['enabled'] = True
self.values['postgresql']['enabled'] = False
print_info("配置外部 PostgreSQL 连接信息")
print_warning("提示: 如果在 kind 集群中访问宿主机服务,使用 'host.docker.internal'")
self.values['externalPostgres']['address'] = prompt(
"PostgreSQL 地址 (kind集群外使用 host.docker.internal 或宿主机IP)",
default="host.docker.internal",
required=True
)
port = prompt("PostgreSQL 端口", default="5432", required=False)
try:
self.values['externalPostgres']['port'] = int(port)
except ValueError:
self.values['externalPostgres']['port'] = 5432
# 配置各个数据库的凭证 - 交互式获取每个数据库的配置信息
databases_config = [
{'key': 'dify', 'name': 'dify', 'desc': '主数据库'},
{'key': 'plugin_daemon', 'name': 'plugin_daemon', 'desc': '插件守护进程数据库'},
{'key': 'enterprise', 'name': 'enterprise', 'desc': '企业版数据库'},
{'key': 'audit', 'name': 'audit', 'desc': '审计数据库'}
]
for db_config in databases_config:
print(f"\n{'='*60}")
print(f"配置数据库: {db_config['name']} ({db_config['desc']})")
print(f"{'='*60}")
db_key = db_config['key']
self.values['externalPostgres']['credentials'][db_key]['database'] = prompt(
f"{db_config['name']} 数据库名",
default=db_config['name'],
required=False
)
self.values['externalPostgres']['credentials'][db_key]['username'] = prompt(
f"{db_config['name']} 用户名",
default="postgres",
required=False
)
self.values['externalPostgres']['credentials'][db_key]['password'] = prompt(
f"{db_config['name']} 密码",
required=True
)
self.values['externalPostgres']['credentials'][db_key]['sslmode'] = prompt_choice(
f"{db_config['name']} SSL 模式",
["disable", "require", "verify-ca", "verify-full"],
default="require"
)
# 设置默认值(不再询问)
self.values['externalPostgres']['credentials'][db_key]['extras'] = ''
self.values['externalPostgres']['credentials'][db_key]['charset'] = ''
self.values['externalPostgres']['credentials'][db_key]['uriScheme'] = 'postgresql'
else:
self.values['externalPostgres']['enabled'] = False
self.values['postgresql']['enabled'] = True
print_info("将使用 Helm Chart 内置的 PostgreSQL")
if prompt_yes_no("是否配置 PostgreSQL 密码?", default=True):
if prompt_yes_no("是否自动生成密码?", default=True):
self.values['postgresql']['global']['postgresql']['auth']['postgresPassword'] = generate_secret(32)
print_success("已生成 PostgreSQL 密码")
else:
self.values['postgresql']['global']['postgresql']['auth']['postgresPassword'] = prompt(
"PostgreSQL root 密码",
required=True
)
# Redis
print_section("Redis 缓存配置")
use_external_redis = prompt_yes_no("是否使用外部 Redis?", default=True)
if use_external_redis:
self.values['externalRedis']['enabled'] = True
self.values['redis']['enabled'] = False
print_info("配置外部 Redis 连接信息")
print_warning("提示: 如果在 kind 集群中访问宿主机服务,使用 'host.docker.internal'")
self.values['externalRedis']['host'] = prompt(
"Redis 主机地址 (kind集群外使用 host.docker.internal 或宿主机IP)",
default="host.docker.internal",
required=True
)
port = prompt("Redis 端口", default="6379", required=False)
try:
self.values['externalRedis']['port'] = int(port)
except ValueError:
self.values['externalRedis']['port'] = 6379
self.values['externalRedis']['useSSL'] = prompt_yes_no("是否使用 SSL?", default=False)
self.values['externalRedis']['username'] = prompt(
"Redis 用户名 (可选)",
default="",
required=False
)
self.values['externalRedis']['password'] = prompt(
"Redis 密码",
required=True
)
db_num = prompt("Redis 数据库编号", default="0", required=False)
try:
self.values['externalRedis']['db'] = int(db_num)
except ValueError:
self.values['externalRedis']['db'] = 0
# Sentinel/Cluster 配置 - 互斥选择
use_sentinel = prompt_yes_no("是否使用 Redis Sentinel?", default=False)
use_cluster = False
if use_sentinel:
self.values['externalRedis']['sentinel']['enabled'] = True
self.values['externalRedis']['cluster']['enabled'] = False
self.values['externalRedis']['sentinel']['nodes'] = prompt(
"Sentinel 节点列表 (host:port,host:port)",
required=True
)
self.values['externalRedis']['sentinel']['serviceName'] = prompt(
"Sentinel 服务名",
required=True
)
self.values['externalRedis']['sentinel']['username'] = prompt(
"Sentinel 用户名 (可选)",
default="",
required=False
)
self.values['externalRedis']['sentinel']['password'] = prompt(
"Sentinel 密码",
required=True
)
socket_timeout = prompt(
"Socket 超时时间(秒)",
default="0.1",
required=False
)
try:
self.values['externalRedis']['sentinel']['socketTimeout'] = float(socket_timeout)
except ValueError:
self.values['externalRedis']['sentinel']['socketTimeout'] = 0.1
else:
self.values['externalRedis']['sentinel']['enabled'] = False
use_cluster = prompt_yes_no("是否使用 Redis Cluster?", default=False)
if use_cluster:
self.values['externalRedis']['cluster']['enabled'] = True
self.values['externalRedis']['cluster']['nodes'] = prompt(
"Cluster 节点列表 (host:port,host:port)",
required=True
)
self.values['externalRedis']['cluster']['password'] = prompt(
"Cluster 密码",
required=True
)
else:
self.values['externalRedis']['cluster']['enabled'] = False
else:
self.values['externalRedis']['enabled'] = False
self.values['redis']['enabled'] = True
print_info("将使用 Helm Chart 内置的 Redis")
if prompt_yes_no("是否配置 Redis 密码?", default=True):
if prompt_yes_no("是否自动生成密码?", default=True):
self.values['redis']['global']['redis']['password'] = generate_secret(32)
print_success("已生成 Redis 密码")
else:
self.values['redis']['global']['redis']['password'] = prompt(
"Redis 密码",
required=True
)
# VectorDB
print_section("向量数据库配置")
use_external_vectordb = prompt_yes_no("是否使用外部向量数据库?", default=True)
self.values['vectorDB']['useExternal'] = use_external_vectordb
if use_external_vectordb:
vectordb_type = prompt_choice(
"选择向量数据库类型",
["qdrant", "weaviate", "milvus", "relyt", "pgvecto-rs",
"tencent", "opensearch", "elasticsearch", "analyticdb", "lindorm"],
default="qdrant"
)
self.values['vectorDB']['externalType'] = vectordb_type
print_info(f"配置外部 {vectordb_type} 连接信息")
print_warning("提示: 如果在 kind 集群中访问宿主机服务,使用 'host.docker.internal'")
if vectordb_type == "qdrant":
self.values['vectorDB']['externalQdrant']['endpoint'] = prompt(
"Qdrant 端点 URL (kind集群外使用 http://host.docker.internal:6333)",
default="http://host.docker.internal:6333",
required=True
)
self.values['vectorDB']['externalQdrant']['apiKey'] = prompt(
"Qdrant API Key",
required=False
)
elif vectordb_type == "weaviate":
self.values['vectorDB']['externalWeaviate']['endpoint'] = prompt(
"Weaviate 端点 URL",
default="http://weaviate:8080",
required=True
)
self.values['vectorDB']['externalWeaviate']['apiKey'] = prompt(
"Weaviate API Key",
required=False
)
# 其他类型可以类似扩展
else:
# 使用内置向量数据库
vectordb_choice = prompt_choice(
"选择内置向量数据库",
["qdrant", "weaviate"],
default="qdrant"
)
if vectordb_choice == "qdrant":
self.values['qdrant']['enabled'] = True
self.values['weaviate']['enabled'] = False
api_key = prompt("Qdrant API Key", default="dify123456", required=False)
self.values['qdrant']['apiKey'] = api_key
replica_count = prompt("Qdrant 副本数", default="3", required=False)
try:
self.values['qdrant']['replicaCount'] = int(replica_count)
except ValueError:
self.values['qdrant']['replicaCount'] = 3
else:
self.values['qdrant']['enabled'] = False
self.values['weaviate']['enabled'] = True
# 存储配置
print_section("存储配置 (Persistence)")
storage_type = prompt_choice(
"选择存储类型",
["local", "s3 (AWS S3 和 S3 兼容协议)", "azure-blob", "aliyun-oss", "google-storage",
"tencent-cos", "volcengine-tos", "huawei-obs"],
default="local"
)
# 处理存储类型选择,将显示名称转换为实际值
if storage_type == "s3 (AWS S3 和 S3 兼容协议)":
storage_type = "s3"
self.values['persistence']['type'] = storage_type
if storage_type == "local":
print_info("配置本地存储")
self.values['persistence']['local']['mountPath'] = prompt(
"挂载路径",
default="/app/api/storage",
required=False
)
storage_class = prompt(
"StorageClass 名称 (留空使用默认)",
default="",
required=False
)
if storage_class:
self.values['persistence']['local']['persistentVolumeClaim']['storageClass'] = storage_class
size = prompt("存储大小", default="5Gi", required=False)
self.values['persistence']['local']['persistentVolumeClaim']['size'] = size
elif storage_type == "s3":
print_info("配置 S3 兼容存储(AWS S3 和 S3 兼容协议)")
# 判断是 AWS S3 还是其他兼容 S3 的服务(如 MinIO)
s3_provider = prompt_choice(
"S3 服务提供商",
["AWS S3", "MinIO", "Cloudflare R2", "其他兼容S3服务"],
default="AWS S3"
)
if s3_provider == "AWS S3":
self.values['persistence']['s3']['useAwsS3'] = True
print_info("配置 AWS S3")
# AWS S3 不需要内置 MinIO
self.values['minio']['enabled'] = False
print_info("已自动关闭内置 MinIO(使用 AWS S3")
else:
self.values['persistence']['s3']['useAwsS3'] = False
print_info(f"配置 {s3_provider} (S3兼容)")
# 非 AWS S3 需要内置 MinIO
self.values['minio']['enabled'] = True
print_info("已自动启用内置 MinIOuseAwsS3=false")
# MinIO 特殊配置说明
if s3_provider == "MinIO":
print_info("")
print_info("=" * 60)
print_info("外部 MinIO 配置说明(对象存储)")
print_info("=" * 60)
print_info("配置您自建的外部 MinIO 服务作为对象存储:")
print_info(" • Access Key = MINIO_ROOT_USER(例如: minioadmin")
print_info(" • Secret Key = MINIO_ROOT_PASSWORD(例如: minioadmin123")
print_info("=" * 60)
print_info("")
default_endpoint = "http://host.docker.internal:9000"
default_access_key = "minioadmin"
default_secret_key = "minioadmin123"
else:
default_endpoint = "https://xxx.r2.cloudflarestorage.com" if s3_provider != "AWS S3" else ""
default_access_key = ""
default_secret_key = ""
self.values['persistence']['s3']['endpoint'] = prompt(
"S3 端点 URL",
default=default_endpoint,
required=(s3_provider != "AWS S3")
)
if s3_provider == "MinIO":
print_info("")
print_info("MinIO 认证信息:")
print_info(" • Access Key = MINIO_ROOT_USER")
print_info(" • Secret Key = MINIO_ROOT_PASSWORD")
print_info("")
self.values['persistence']['s3']['accessKey'] = prompt(
f"Access Key{' (MinIO: MINIO_ROOT_USER)' if s3_provider == 'MinIO' else ''}",
default=default_access_key,
required=True
)
self.values['persistence']['s3']['secretKey'] = prompt(
f"Secret Key{' (MinIO: MINIO_ROOT_PASSWORD)' if s3_provider == 'MinIO' else ''}",
default=default_secret_key,
required=True
)
self.values['persistence']['s3']['region'] = prompt(
"区域",
default="us-east-1",
required=False
)
self.values['persistence']['s3']['bucketName'] = prompt(
"Bucket 名称",
default="your-bucket-name",
required=True
)
address_type = prompt(
"地址类型 (path-style/virtual-hosted-style, 留空使用默认)",
default="",
required=False
)
if address_type:
self.values['persistence']['s3']['addressType'] = address_type
# 如果启用了 MinIO,配置 MinIO
if self.values['minio'].get('enabled', False):
print_section("配置内置 MinIO")
print_info("=" * 60)
print_info("重要说明:内置 MinIO")
print_info("=" * 60)
print_info("此 MinIO 服务用于 plugin 构建,不用于业务对象存储。")
print_info("业务对象存储请使用上面配置的外部 S3 服务。")
print_info("=" * 60)
print_info("")
if prompt_yes_no("是否自动生成 MinIO root 密码?", default=True):
self.values['minio']['rootPassword'] = generate_secret(32)
print_success("已生成 MinIO root 密码")
else:
self.values['minio']['rootPassword'] = prompt(
"MinIO root 密码",
required=True
)
self.values['minio']['rootUser'] = prompt(
"MinIO root 用户",
default="minioadmin",
required=False
)
# MinIO 配置 - 如果存储类型不是 s3,需要启用内置 MinIO
if storage_type != "s3":
print_section("内置 MinIO 配置")
print_info("=" * 60)
print_info("重要说明:内置 MinIO")
print_info("=" * 60)
print_info("此 MinIO 服务用于 plugin 构建,不用于业务对象存储。")
print_info("业务对象存储请使用 persistence.type 配置的外部存储服务。")
print_info("=" * 60)
print_info("")
self.values['minio']['enabled'] = True
if prompt_yes_no("是否自动生成 MinIO root 密码?", default=True):
self.values['minio']['rootPassword'] = generate_secret(32)
print_success("已生成 MinIO root 密码")
else:
self.values['minio']['rootPassword'] = prompt(
"MinIO root 密码",
required=True
)
self.values['minio']['rootUser'] = prompt(
"MinIO root 用户",
default="minioadmin",
required=False
)
# ==================== 模块 3: 网络配置 ====================
def configure_networking(self):
"""配置网络"""
print_header("模块 3: 网络配置 (Networking)")
# TLS 配置 - 放在网络配置模块,与 Ingress 联动
print_section("TLS 配置")
print_info("TLS 配置影响内部服务通信和 CORS 设置")
print_warning("注意: TLS 配置必须与 Ingress 配置一致,否则会出现 CORS 跨域问题")
use_tls = prompt_yes_no("是否启用 TLS (内部服务)?", default=False)
self.values['global']['useTLS'] = use_tls
if use_tls:
print_info("已启用 TLS,后续 Ingress 配置也需要启用 TLS")
# Ingress 配置
print_section("Ingress 配置")
# 企业版默认启用 Ingress
self.values['ingress']['enabled'] = True
print_info("已自动启用 Ingress(企业版默认配置)")
# Ingress Class 选择
print_info("")
print_info("请选择 Ingress Controller 类型:")
ingress_class_choice = prompt_choice(
"Ingress Class 名称",
["nginx", "alb", "traefik", "istio", "其他"],
default="nginx"
)
if ingress_class_choice == "nginx":
self.values['ingress']['className'] = "nginx"
print_info("")
print_warning("提示: 请确保已安装 NGINX Ingress Controller")
print_info("安装方法: kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml")
elif ingress_class_choice == "alb":
self.values['ingress']['className'] = "alb"
print_info("")
print_warning("提示: 请确保已安装 AWS Load Balancer Controller")
print_info("安装方法: 参考 AWS EKS 文档配置 ALB Ingress Controller")
elif ingress_class_choice == "traefik":
self.values['ingress']['className'] = "traefik"
print_info("")
print_warning("提示: 请确保已安装 Traefik Ingress Controller")
elif ingress_class_choice == "istio":
self.values['ingress']['className'] = "istio"
print_info("")
print_warning("提示: 请确保已安装 Istio Gateway")
else:
# 其他选项,手动输入
self.values['ingress']['className'] = prompt(
"请输入 Ingress Class 名称",
default="",
required=False
)
# Ingress TLS 配置 - 与全局 TLS 联动
if use_tls:
print_info("由于已启用全局 TLS,建议 Ingress 也启用 TLS")
ingress_tls = prompt_yes_no("是否在 Ingress 中配置 TLS?", default=True)
else:
ingress_tls = prompt_yes_no("是否在 Ingress 中配置 TLS?", default=False)
if ingress_tls:
print_info("TLS 证书配置:")
print_info(" 1. 可以通过 cert-manager 自动管理 (使用 annotations)")
print_info(" 2. 或者手动配置 TLS Secret")
# TLS 配置示例
tls_hosts = prompt(
"TLS 主机列表 (逗号分隔, 例如: dify.example.com,api.dify.example.com)",
default="",
required=False
)
if tls_hosts:
hosts_list = [h.strip() for h in tls_hosts.split(',') if h.strip()]
if hosts_list:
# 创建 TLS 配置
if 'tls' not in self.values['ingress'] or not isinstance(self.values['ingress']['tls'], list):
self.values['ingress']['tls'] = []
self.values['ingress']['tls'].append({
'hosts': hosts_list,
'secretName': prompt(
"TLS Secret 名称",
default=f"{hosts_list[0]}-tls",
required=False
) or f"{hosts_list[0]}-tls"
})
# 添加 cert-manager 注解示例
if prompt_yes_no("是否使用 cert-manager 自动管理证书?", default=False):
if 'annotations' not in self.values['ingress']:
self.values['ingress']['annotations'] = {}
cluster_issuer = prompt(
"ClusterIssuer 名称 (例如: letsencrypt-prod)",
default="",
required=False
)
if cluster_issuer:
self.values['ingress']['annotations']['cert-manager.io/cluster-issuer'] = cluster_issuer
print_success(f"已配置 cert-manager ClusterIssuer: {cluster_issuer}")
# 检查 TLS 一致性
if use_tls and not ingress_tls:
print_warning("警告: 全局 TLS 已启用,但 Ingress TLS 未启用,可能导致 CORS 问题")
if prompt_yes_no("是否现在启用 Ingress TLS?", default=True):
ingress_tls = True
# 重新配置 TLS
tls_hosts = prompt(
"TLS 主机列表 (逗号分隔)",
default="",
required=False
)
if tls_hosts:
hosts_list = [h.strip() for h in tls_hosts.split(',') if h.strip()]
if hosts_list:
if 'tls' not in self.values['ingress'] or not isinstance(self.values['ingress']['tls'], list):
self.values['ingress']['tls'] = []
self.values['ingress']['tls'].append({
'hosts': hosts_list,
'secretName': prompt(
"TLS Secret 名称",
default=f"{hosts_list[0]}-tls",
required=False
) or f"{hosts_list[0]}-tls"
})
if not use_tls and ingress_tls:
print_warning("警告: Ingress TLS 已启用,但全局 TLS 未启用,建议保持一致")
if prompt_yes_no("是否启用全局 TLS?", default=True):
self.values['global']['useTLS'] = True
use_tls = True
# useIpAsHost 配置 - 企业版不支持,固定为 False
self.values['ingress']['useIpAsHost'] = False
# ==================== 模块 4: 邮件配置 ====================
def configure_mail(self):
"""配置邮件"""
print_header("模块 4: 邮件配置 (Mail Configuration)")
mail_type = prompt_choice(
"选择邮件服务类型",
["", "resend", "smtp"],
default=""
)
self.values['mail']['type'] = mail_type
if mail_type:
self.values['mail']['defaultSender'] = prompt(
"默认发件人地址 (例如: no-reply <no-reply@dify.ai>)",
default="YOUR EMAIL FROM (eg: no-reply <no-reply@dify.ai>)",
required=False
)
if mail_type == "resend":
self.values['mail']['resend']['apiKey'] = prompt(
"Resend API Key",
required=True
)
self.values['mail']['resend']['apiUrl'] = prompt(
"Resend API URL",
default="https://api.resend.com",
required=False
)
elif mail_type == "smtp":
self.values['mail']['smtp']['server'] = prompt(
"SMTP 服务器",
required=True
)
port = prompt("SMTP 端口", default="587", required=False)
try:
self.values['mail']['smtp']['port'] = int(port)
except ValueError:
self.values['mail']['smtp']['port'] = 587
self.values['mail']['smtp']['username'] = prompt(
"SMTP 用户名",
required=True
)
self.values['mail']['smtp']['password'] = prompt(
"SMTP 密码",
required=True
)
self.values['mail']['smtp']['useTLS'] = prompt_yes_no(
"是否使用 TLS?",
default=False
)
# ==================== 模块 5: 服务配置 ====================
def configure_services(self):
"""配置服务"""
print_header("模块 5: 服务配置 (Services Configuration)")
# Enterprise 相关配置
if self.values.get('enterprise', {}).get('enabled', True):
print_section("Enterprise 服务配置")
# 所有密钥都按注释自动生成
print_info("Enterprise appSecretKey 将使用 openssl rand -base64 42 自动生成")
self.values['enterprise']['appSecretKey'] = generate_secret(42)
print_success(f"已生成 Enterprise appSecretKey: {self.values['enterprise']['appSecretKey'][:20]}...")
print_info("adminAPIsSecretKeySalt 将使用 openssl rand -base64 42 自动生成")
self.values['enterprise']['adminAPIsSecretKeySalt'] = generate_secret(42)
print_success(f"已生成 adminAPIsSecretKeySalt: {self.values['enterprise']['adminAPIsSecretKeySalt'][:20]}...")
print_info("passwordEncryptionKey 将使用 openssl rand -base64 32 自动生成 (32-byte AES-256 key)")
self.values['enterprise']['passwordEncryptionKey'] = generate_secret(32)
print_success(f"已生成 passwordEncryptionKey: {self.values['enterprise']['passwordEncryptionKey'][:20]}...")
license_mode = prompt_choice(
"License 模式",
["online", "offline"],
default="online"
)
self.values['enterprise']['licenseMode'] = license_mode
if license_mode == "online":
# 使用默认 License 服务器 URL,不询问用户
self.values['enterprise']['licenseServer'] = "https://licenses.dify.ai/server"
print_info(f"License 服务器 URL: {self.values['enterprise']['licenseServer']}")
print_section("服务启用状态")
print_info("可以跳过此部分使用默认值,或根据需要调整")
if prompt_yes_no("是否配置服务启用状态?", default=False):
services = ['api', 'worker', 'workerBeat', 'web', 'sandbox',
'enterprise', 'enterpriseAudit', 'enterpriseFrontend',
'ssrfProxy', 'unstructured', 'plugin_daemon', 'plugin_manager']
for service in services:
if service in self.values:
current = self.values[service].get('enabled', True)
self.values[service]['enabled'] = prompt_yes_no(
f"是否启用 {service}?",
default=current
)
# ==================== 主流程 ====================
def generate(self):
"""生成配置"""
print_header("Dify Helm Chart Values 生成器")
print_info("此工具将引导您完成 values-prd.yaml 的配置")
print_info("您可以随时按 Ctrl+C 退出")
try:
# 按顺序配置各个模块
self.configure_global()
self.configure_infrastructure()
self.configure_networking()
self.configure_mail()
self.configure_services()
# 保存文件
output_file = "values-prd.yaml"
if os.path.exists(output_file):
if not prompt_yes_no(f"{output_file} 已存在,是否覆盖?", default=False):
output_file = prompt("请输入新的文件名", default="values-prd.yaml", required=False)
if not output_file:
output_file = "values-prd.yaml"
self.save(output_file)
print_header("配置完成!")
print_success(f"配置文件已保存到: {output_file}")
print_info("请检查配置文件并根据需要进行调整")
print_info("然后可以使用: helm install <release-name> . -f values-prd.yaml")
except KeyboardInterrupt:
print("\n\n")
print_warning("用户中断操作")
if prompt_yes_no("是否保存当前进度?", default=False):
output_file = prompt("请输入文件名", default="values-prd-partial.yaml", required=False)
if output_file:
self.save(output_file)
print_success(f"部分配置已保存到: {output_file}")
sys.exit(0)
except Exception as e:
print_error(f"生成配置时出错: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
def main():
"""主函数"""
source_file = "values.yaml"
if not os.path.exists(source_file):
print_error(f"模板文件不存在: {source_file}")
sys.exit(1)
generator = ValuesGenerator(source_file)
generator.generate()
if __name__ == "__main__":
main()