Add feature-based architecture for version-specific config and Add support for 3.7.x (#3)

* chore: upgrade tj-actions/changed-files to v46.0.1

* Add feature-based architecture for version-specific config

Introduces a feature registry and auto-discovery system under modules/features/ to manage version-specific configuration logic. Adds base classes and decorators for feature registration, semantic version comparison, and applies features in global, infrastructure, plugins, and services modules. Implements features for trigger worker, plugin metrics, and external Prometheus (3.7.0+). Updates i18n translations and documentation to reflect the new architecture and configuration options. Includes tests for feature registration, version parsing, and applicability.
This commit is contained in:
Petrus Han
2025-12-30 16:58:02 +08:00
committed by GitHub
parent 2894e65655
commit 7ba7a2e601
12 changed files with 1191 additions and 2 deletions
+133 -1
View File
@@ -1,5 +1,114 @@
# Dify EE(企业版)Helm Chart Values 生成器流程图
## 版本特性架构(Feature-based Architecture
从 3.7.0 版本开始,我们采用了 **特性驱动 + 版本检测** 的混合架构来处理不同版本之间的配置差异。
### 架构图
```mermaid
flowchart TD
subgraph VersionManager["版本管理器"]
VM[VersionManager] --> |解析版本| ParseVersion["parse_version()"]
VM --> |比较版本| CompareVersion["compare_versions()"]
VM --> |检查约束| VersionSatisfies["version_satisfies()"]
end
subgraph FeatureSystem["特性系统 (modules/features/)"]
FR[FeatureRegistry] --> |注册| Register["register_feature()"]
FR --> |发现| Discover["_discover_features()"]
FR --> |应用| Apply["apply_features()"]
subgraph Features["版本特性模块"]
F1["trigger_domain.py<br/>≥3.7.0, global"]
F2["trigger_worker.py<br/>≥3.7.0, services"]
F3["plugin_metric.py<br/>≥3.7.0, plugins"]
F4["external_prometheus.py<br/>≥3.7.0, infrastructure"]
end
Register --> Features
end
subgraph Modules["配置模块 (modules/)"]
M1["global_config.py"]
M2["networking.py"]
M3["plugins.py"]
M4["services.py"]
end
Generator["ValuesGenerator"] --> |chart_version| VM
Generator --> Modules
Modules --> |调用| Apply
Apply --> |查询适用特性| FR
FR --> |版本匹配| VersionSatisfies
VersionSatisfies --> |返回匹配特性| Features
Features --> |配置| Generator
style FeatureSystem fill:#E6FFE6
style VersionManager fill:#E6F3FF
style Features fill:#FFF4E6
```
### 目录结构
```
modules/
├── __init__.py
├── global_config.py # 核心配置(所有版本通用)
├── infrastructure.py
├── networking.py
├── mail.py
├── plugins.py
├── services.py
└── features/ # 版本特定特性
├── __init__.py # 自动发现机制
├── base.py # Feature基类、注册器、版本比较
├── trigger_domain.py # 3.7.0+ triggerDomain 配置
├── trigger_worker.py # 3.7.0+ triggerWorker 服务
├── plugin_metric.py # 3.7.0+ 插件指标监控
└── external_prometheus.py # 3.7.0+ 外部 Prometheus
```
### 工作原理
1. **启动时自动发现**`modules/features/__init__.py` 自动扫描并导入所有特性模块
2. **装饰器注册**:每个特性使用 `@register_feature()` 装饰器声明版本约束
3. **配置时应用**:各模块末尾调用 `apply_features(generator, "module_name")`
4. **版本过滤**:只有满足版本约束的特性才会被执行
### 添加新特性示例
```python
# modules/features/new_feature.py
from .base import Feature, register_feature
from utils import print_section, prompt
from i18n import get_translator
@register_feature(
min_version="3.8.0", # 最小版本(含)
max_version=None, # 最大版本(含),None表示无上限
module="global", # 所属模块
name="New Feature",
description="Description of the feature"
)
class NewFeature(Feature):
def configure(self, generator) -> None:
_t = get_translator()
# 配置逻辑...
generator.values['global']['newField'] = "value"
```
### 版本比较规则
```
3.6.0-alpha.1 < 3.6.0-beta.1 < 3.6.0-rc.1 < 3.6.0 < 3.7.0
```
- 预发布版本(alpha < beta < rc)优先级低于正式版本
- 支持语义化版本比较
---
## 主流程图
```mermaid
@@ -56,13 +165,26 @@ flowchart TD
RAGConfig[配置RAG参数<br/>keywordDataSourceType<br/>topKMaxValue<br/>indexingMaxSegmentationTokensLength]
RAGConfig --> End([模块1完成])
RAGConfig --> ApplyFeatures{应用版本特性}
ApplyFeatures --> CheckVersion{chart_version<br/>≥ 3.7.0?}
CheckVersion -->|是| TriggerDomain[配置 triggerDomain<br/>工作流 Webhook 触发器域名]
CheckVersion -->|否| End
TriggerDomain --> CheckFuture{更多版本特性?}
CheckFuture -->|有| ApplyMore[应用其他特性...]
CheckFuture -->|无| End
ApplyMore --> End
End([模块1完成])
style Start fill:#E6F3FF
style End fill:#E6F3FF
style RAGCheck fill:#FFF4E6
style DisableUnstructured fill:#FFE6E6
style EnableUnstructured fill:#E6FFE6
style ApplyFeatures fill:#E6FFE6
style TriggerDomain fill:#90EE90
style CheckVersion fill:#FFF4E6
```
## 模块2: 基础设施配置流程图
@@ -328,6 +450,16 @@ flowchart TD
## 决策点说明
### 版本特性对照表
| 特性名称 | 最低版本 | 所属模块 | 说明 |
|---------|---------|---------|------|
| `triggerDomain` | 3.7.0 | global | 工作流 Webhook 触发器域名配置 |
| `triggerWorker` | 3.7.0 | services | Trigger Worker 服务配置(副本数、Celery、代码限制) |
| `plugin_manager.metric` | 3.7.0 | plugins | 插件资源监控配置(CPU、内存、网络 I/O) |
| `externalPrometheus` | 3.7.0 | infrastructure | 外部 Prometheus 配置(用于插件指标监控) |
| `ssrfProxy.sandboxHost` | 3.6.x | infrastructure (高级选项) | 自定义 sandbox 主机 FQDN(用于跨命名空间部署) |
### 关键决策点
1. **PostgreSQL选择** (互斥)
+118
View File
@@ -254,6 +254,12 @@ TRANSLATIONS = {
'minio_root_password': 'MinIO Root Password',
'minio_root_user': 'MinIO Root User',
# Advanced configuration
'advanced_config': 'Advanced Configuration',
'config_advanced_options': 'Configure advanced options? (for special deployment scenarios)',
'ssrf_proxy_sandbox_host': 'SSRF Proxy Sandbox Host (leave empty for default)',
'ssrf_proxy_sandbox_host_desc': 'Custom sandbox host FQDN for cross-namespace deployments (e.g.: my-release-dify-sandbox-svc.my-namespace.svc.cluster.local)',
'select_mail_service_type': 'Select Mail Service Type',
'default_sender_address': 'Default Sender Address (e.g.: no-reply <no-reply@dify.ai>)',
'resend_api_key': 'Resend API Key',
@@ -358,6 +364,59 @@ TRANSLATIONS = {
'http_selected': 'HTTP protocol selected (not recommended)',
'https_selected': 'HTTPS protocol selected (recommended)',
# Version-specific features
'applying_feature': 'Applying version-specific feature',
'trigger_domain_desc': 'Trigger domain is used for workflow webhook triggers (3.7.0+)',
'trigger_domain': 'Trigger Domain (for workflow webhooks)',
# Trigger Worker Feature (3.7.0+)
'trigger_worker_config': 'Trigger Worker Configuration (3.7.0+)',
'trigger_worker_desc': 'Trigger Worker is a dedicated Celery worker for handling workflow triggers',
'config_trigger_worker': 'Configure Trigger Worker settings?',
'using_default_trigger_worker': 'Using default Trigger Worker settings',
'trigger_worker_replicas': 'Trigger Worker Replicas',
'trigger_worker_celery_amount': 'Celery Worker Amount',
'trigger_worker_code_limits_desc': 'Code execution limits control sandbox execution parameters',
'config_trigger_worker_code_limits': 'Configure code execution limits?',
'max_string_array_length': 'Max String Array Length',
'max_object_array_length': 'Max Object Array Length',
'max_number_array_length': 'Max Number Array Length',
'trigger_worker_configured': 'Trigger Worker configured successfully',
# Plugin Metric Feature (3.7.0+)
'plugin_metric_config': 'Plugin Metrics Configuration (3.7.0+)',
'plugin_metric_desc': 'Plugin metrics monitoring for CPU, memory, and network I/O',
'config_plugin_metric': 'Configure plugin metrics?',
'using_default_plugin_metric': 'Using default plugin metrics settings (disabled)',
'plugin_metric_source_options': 'Available metric sources:',
'plugin_metric_disabled_desc': 'No metrics collected',
'plugin_metric_cadvisor_desc': 'Collect from cadvisor (requires cluster roles)',
'plugin_metric_prometheus_desc': 'Query from external Prometheus (recommended)',
'plugin_metric_source': 'Metric Source',
'cadvisor_cluster_role_warning': 'Warning: cadvisor requires cluster roles for reading nodes',
'config_cadvisor_scrape': 'Configure cadvisor scrape settings?',
'scrape_interval': 'Scrape Interval (e.g., 20s)',
'scrape_timeout': 'Scrape Timeout (e.g., 10s)',
'retain_period': 'Retain Period (e.g., 604800s)',
'prometheus_external_required': 'Note: prometheus source requires externalPrometheus to be enabled',
'prometheus_config_in_infrastructure': 'External Prometheus will be configured in infrastructure module',
'plugin_metric_configured': 'Plugin metrics configured successfully',
# External Prometheus Feature (3.7.0+)
'external_prometheus_config': 'External Prometheus Configuration (3.7.0+)',
'external_prometheus_desc': 'External Prometheus for plugin metrics monitoring',
'enable_external_prometheus': 'Enable External Prometheus?',
'external_prometheus_disabled': 'External Prometheus disabled',
'external_prometheus_endpoint_desc': 'Prometheus endpoint for querying metrics',
'prometheus_endpoint': 'Prometheus Endpoint URL',
'prometheus_timeout': 'Query Timeout (e.g., 10s)',
'prometheus_auth_required': 'Prometheus requires authentication?',
'prometheus_username': 'Prometheus Username',
'prometheus_password': 'Prometheus Password',
'prometheus_insecure': 'Skip TLS verification?',
'prometheus_insecure_warning': 'Warning: TLS verification disabled, not recommended for production',
'external_prometheus_configured': 'External Prometheus configured successfully',
},
'zh': {
# Common
@@ -733,6 +792,12 @@ TRANSLATIONS = {
'minio_root_password': 'MinIO root 密码',
'minio_root_user': 'MinIO root 用户',
# Advanced configuration
'advanced_config': '高级配置',
'config_advanced_options': '是否配置高级选项?(用于特殊部署场景)',
'ssrf_proxy_sandbox_host': 'SSRF 代理 Sandbox 主机 (留空使用默认值)',
'ssrf_proxy_sandbox_host_desc': '自定义 sandbox 主机 FQDN,用于跨命名空间部署 (例如: my-release-dify-sandbox-svc.my-namespace.svc.cluster.local)',
'select_mail_service_type': '选择邮件服务类型',
'default_sender_address': '默认发件人地址 (例如: no-reply <no-reply@dify.ai>)',
'resend_api_key': 'Resend API Key',
@@ -837,6 +902,59 @@ TRANSLATIONS = {
'http_selected': '已选择 HTTP 协议(不推荐)',
'https_selected': '已选择 HTTPS 协议(推荐)',
# Version-specific features
'applying_feature': '正在应用版本特性',
'trigger_domain_desc': 'Trigger Domain 用于工作流 Webhook 触发器 (3.7.0+)',
'trigger_domain': 'Trigger Domain (工作流 Webhook 触发器)',
# Trigger Worker Feature (3.7.0+)
'trigger_worker_config': 'Trigger Worker 配置 (3.7.0+)',
'trigger_worker_desc': 'Trigger Worker 是专用于处理工作流触发器的 Celery Worker',
'config_trigger_worker': '是否配置 Trigger Worker 设置?',
'using_default_trigger_worker': '使用默认 Trigger Worker 设置',
'trigger_worker_replicas': 'Trigger Worker 副本数',
'trigger_worker_celery_amount': 'Celery Worker 数量',
'trigger_worker_code_limits_desc': '代码执行限制控制沙箱执行参数',
'config_trigger_worker_code_limits': '是否配置代码执行限制?',
'max_string_array_length': '最大字符串数组长度',
'max_object_array_length': '最大对象数组长度',
'max_number_array_length': '最大数字数组长度',
'trigger_worker_configured': 'Trigger Worker 配置成功',
# Plugin Metric Feature (3.7.0+)
'plugin_metric_config': '插件指标监控配置 (3.7.0+)',
'plugin_metric_desc': '插件资源监控:CPU、内存、网络 I/O',
'config_plugin_metric': '是否配置插件指标监控?',
'using_default_plugin_metric': '使用默认插件指标设置(已禁用)',
'plugin_metric_source_options': '可用的指标来源:',
'plugin_metric_disabled_desc': '不收集指标',
'plugin_metric_cadvisor_desc': '从 cadvisor 收集(需要集群角色)',
'plugin_metric_prometheus_desc': '从外部 Prometheus 查询(推荐)',
'plugin_metric_source': '指标来源',
'cadvisor_cluster_role_warning': '警告:cadvisor 需要读取节点的集群角色权限',
'config_cadvisor_scrape': '是否配置 cadvisor 抓取设置?',
'scrape_interval': '抓取间隔(如 20s',
'scrape_timeout': '抓取超时(如 10s',
'retain_period': '保留周期(如 604800s',
'prometheus_external_required': '注意:prometheus 来源需要启用 externalPrometheus',
'prometheus_config_in_infrastructure': '外部 Prometheus 将在基础设施模块中配置',
'plugin_metric_configured': '插件指标监控配置成功',
# External Prometheus Feature (3.7.0+)
'external_prometheus_config': '外部 Prometheus 配置 (3.7.0+)',
'external_prometheus_desc': '用于插件指标监控的外部 Prometheus',
'enable_external_prometheus': '是否启用外部 Prometheus',
'external_prometheus_disabled': '外部 Prometheus 已禁用',
'external_prometheus_endpoint_desc': '用于查询指标的 Prometheus 端点',
'prometheus_endpoint': 'Prometheus 端点 URL',
'prometheus_timeout': '查询超时(如 10s',
'prometheus_auth_required': 'Prometheus 是否需要认证?',
'prometheus_username': 'Prometheus 用户名',
'prometheus_password': 'Prometheus 密码',
'prometheus_insecure': '是否跳过 TLS 验证?',
'prometheus_insecure_warning': '警告:已禁用 TLS 验证,不建议在生产环境使用',
'external_prometheus_configured': '外部 Prometheus 配置成功',
}
}
+57
View File
@@ -0,0 +1,57 @@
"""
Version-specific features module
This module provides a framework for managing version-specific configuration features.
Each feature declares its applicable version range and is automatically loaded when
the current chart version matches.
Architecture:
- FeatureRegistry: Central registry for all version-specific features
- Feature: Base class for defining version-specific configuration logic
- Each feature module (e.g., trigger_domain.py) registers features with version constraints
Usage:
1. Create a new feature file in modules/features/
2. Inherit from Feature base class
3. Use @register_feature decorator to register with version constraints
4. Features are automatically discovered and applied during configuration
Example:
from modules.features import Feature, register_feature
@register_feature(min_version="3.7.0", module="global")
class TriggerDomainFeature(Feature):
def configure(self, generator):
# Configuration logic here
pass
"""
from .base import Feature, FeatureRegistry, register_feature, apply_features
__all__ = [
'Feature',
'FeatureRegistry',
'register_feature',
'apply_features',
]
# Auto-discover and import all feature modules
def _discover_features():
"""Automatically discover and import all feature modules"""
import os
import importlib
from pathlib import Path
features_dir = Path(__file__).parent
for file in features_dir.glob("*.py"):
if file.name.startswith("_") or file.name == "base.py":
continue
module_name = file.stem
try:
importlib.import_module(f".{module_name}", package=__name__)
except ImportError as e:
print(f"Warning: Failed to import feature module {module_name}: {e}")
# Discover features on module load
_discover_features()
+275
View File
@@ -0,0 +1,275 @@
"""
Feature base classes and registry for version-specific configurations
This module provides the core infrastructure for managing version-specific features:
- Semantic version comparison
- Feature registration with version constraints
- Automatic feature discovery and application
"""
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Callable, Any, Type
from functools import wraps
import re
def parse_version(version_str: str) -> tuple:
"""
Parse version string into comparable tuple
Handles versions like:
- "3.7.0" -> (3, 7, 0, "", 0)
- "3.6.0-beta.1" -> (3, 6, 0, "beta", 1)
- "3.6.0-alpha.2" -> (3, 6, 0, "alpha", 2)
- "3.6.0-rc.1" -> (3, 6, 0, "rc", 1)
Pre-release versions are considered less than release versions:
3.6.0-alpha.1 < 3.6.0-beta.1 < 3.6.0-rc.1 < 3.6.0
"""
if not version_str:
return (0, 0, 0, "", 0)
# Split main version and pre-release
parts = version_str.split("-", 1)
main_version = parts[0]
pre_release = parts[1] if len(parts) > 1 else ""
# Parse main version (major.minor.patch)
version_parts = main_version.split(".")
major = int(version_parts[0]) if len(version_parts) > 0 else 0
minor = int(version_parts[1]) if len(version_parts) > 1 else 0
patch = int(version_parts[2]) if len(version_parts) > 2 else 0
# Parse pre-release (e.g., "beta.1", "alpha.2", "rc.1")
pre_type = ""
pre_num = 0
if pre_release:
pre_match = re.match(r"([a-zA-Z]+)\.?(\d+)?", pre_release)
if pre_match:
pre_type = pre_match.group(1).lower()
pre_num = int(pre_match.group(2)) if pre_match.group(2) else 0
# Pre-release priority: "" (release) > "rc" > "beta" > "alpha"
# Use higher number for release (empty string = highest priority)
pre_priority = {
"": 999, # Release version has highest priority
"rc": 3,
"beta": 2,
"alpha": 1,
}
return (major, minor, patch, pre_priority.get(pre_type, 0), pre_num)
def compare_versions(v1: str, v2: str) -> int:
"""
Compare two version strings
Returns:
-1 if v1 < v2
0 if v1 == v2
1 if v1 > v2
"""
parsed_v1 = parse_version(v1)
parsed_v2 = parse_version(v2)
if parsed_v1 < parsed_v2:
return -1
elif parsed_v1 > parsed_v2:
return 1
return 0
def version_satisfies(version: str, min_version: Optional[str] = None, max_version: Optional[str] = None) -> bool:
"""
Check if version satisfies the given constraints
Args:
version: The version to check
min_version: Minimum version (inclusive), None means no minimum
max_version: Maximum version (inclusive), None means no maximum
Returns:
True if version satisfies constraints
"""
if min_version and compare_versions(version, min_version) < 0:
return False
if max_version and compare_versions(version, max_version) > 0:
return False
return True
class Feature(ABC):
"""
Base class for version-specific features
Each feature must implement:
- name: Human-readable feature name
- configure(): Configuration logic to apply
"""
# Feature metadata (set by decorator or subclass)
name: str = ""
description: str = ""
min_version: Optional[str] = None
max_version: Optional[str] = None
module: str = "" # Which module this feature belongs to (e.g., "global", "networking")
@abstractmethod
def configure(self, generator) -> None:
"""
Apply feature configuration
Args:
generator: ValuesGenerator instance
"""
pass
def is_applicable(self, chart_version: str) -> bool:
"""
Check if this feature applies to the given chart version
Args:
chart_version: Helm Chart version string
Returns:
True if feature should be applied
"""
return version_satisfies(chart_version, self.min_version, self.max_version)
class FeatureRegistry:
"""
Central registry for all version-specific features
Features are registered with version constraints and module associations.
During configuration, applicable features are automatically applied.
"""
_features: Dict[str, List[Type[Feature]]] = {}
@classmethod
def register(cls, feature_class: Type[Feature], module: str = "") -> None:
"""
Register a feature class
Args:
feature_class: Feature class to register
module: Module this feature belongs to
"""
if module not in cls._features:
cls._features[module] = []
cls._features[module].append(feature_class)
@classmethod
def get_features_for_module(cls, module: str, chart_version: str) -> List[Feature]:
"""
Get all applicable features for a module and version
Args:
module: Module name (e.g., "global", "networking")
chart_version: Helm Chart version
Returns:
List of Feature instances that apply to this version
"""
features = []
for feature_class in cls._features.get(module, []):
feature = feature_class()
if feature.is_applicable(chart_version):
features.append(feature)
return features
@classmethod
def get_all_features(cls, chart_version: str) -> Dict[str, List[Feature]]:
"""
Get all applicable features grouped by module
Args:
chart_version: Helm Chart version
Returns:
Dict mapping module names to lists of applicable features
"""
result = {}
for module, feature_classes in cls._features.items():
applicable = []
for feature_class in feature_classes:
feature = feature_class()
if feature.is_applicable(chart_version):
applicable.append(feature)
if applicable:
result[module] = applicable
return result
@classmethod
def clear(cls) -> None:
"""Clear all registered features (useful for testing)"""
cls._features.clear()
def register_feature(
min_version: Optional[str] = None,
max_version: Optional[str] = None,
module: str = "global",
name: str = "",
description: str = ""
) -> Callable[[Type[Feature]], Type[Feature]]:
"""
Decorator to register a feature with version constraints
Args:
min_version: Minimum chart version (inclusive)
max_version: Maximum chart version (inclusive)
module: Module this feature belongs to
name: Human-readable feature name
description: Feature description
Example:
@register_feature(min_version="3.7.0", module="global")
class TriggerDomainFeature(Feature):
def configure(self, generator):
pass
"""
def decorator(cls: Type[Feature]) -> Type[Feature]:
cls.min_version = min_version
cls.max_version = max_version
cls.module = module
if name:
cls.name = name
if description:
cls.description = description
FeatureRegistry.register(cls, module)
return cls
return decorator
def apply_features(generator, module: str) -> None:
"""
Apply all applicable features for a module
This function should be called at the end of each module's configure function
to apply version-specific features.
Args:
generator: ValuesGenerator instance
module: Module name (e.g., "global", "networking")
"""
from utils import print_info, print_success
from i18n import get_translator
_t = get_translator()
chart_version = generator.chart_version
if not chart_version:
return
features = FeatureRegistry.get_features_for_module(module, chart_version)
for feature in features:
feature_name = feature.name or feature.__class__.__name__
print_info(f"{_t('applying_feature')}: {feature_name} (>= {feature.min_version})")
feature.configure(generator)
print_success(f"{feature_name}")
+103
View File
@@ -0,0 +1,103 @@
"""
External Prometheus Feature - Available from Chart version 3.7.0+
This feature adds externalPrometheus configuration for plugin metrics monitoring.
When enabled, plugin_manager can query metrics from an external Prometheus server.
"""
from .base import Feature, register_feature
from utils import print_section, print_info, print_success, print_warning, prompt, prompt_yes_no
from i18n import get_translator
@register_feature(
min_version="3.7.0",
module="infrastructure",
name="External Prometheus",
description="Configure external Prometheus for plugin metrics monitoring"
)
class ExternalPrometheusFeature(Feature):
"""
Configure externalPrometheus settings (3.7.0+)
This feature provides an external Prometheus endpoint for plugin metrics:
- Required when plugin_manager.metric.source = "prometheus"
- Avoids the need for cluster roles required by cadvisor
- Supports authentication (username/password)
Required metrics:
- container_cpu_usage_seconds_total
- container_memory_working_set_bytes
- container_network_receive_bytes_total
- container_network_transmit_bytes_total
"""
def configure(self, generator) -> None:
"""Configure external Prometheus settings"""
_t = get_translator()
print_section(_t('external_prometheus_config'))
print_info(_t('external_prometheus_desc'))
# Check if user wants to enable external Prometheus
enable_prometheus = prompt_yes_no(
_t('enable_external_prometheus'),
default=generator.values.get('externalPrometheus', {}).get('enabled', False)
)
# Ensure externalPrometheus configuration exists
if 'externalPrometheus' not in generator.values:
generator.values['externalPrometheus'] = {}
generator.values['externalPrometheus']['enabled'] = enable_prometheus
if not enable_prometheus:
print_info(_t('external_prometheus_disabled'))
return
# Configure Prometheus endpoint
print_info(_t('external_prometheus_endpoint_desc'))
endpoint = prompt(
_t('prometheus_endpoint'),
default=generator.values.get('externalPrometheus', {}).get('endpoint', 'http://prometheus:9090'),
required=True
)
generator.values['externalPrometheus']['endpoint'] = endpoint
# Configure timeout
timeout = prompt(
_t('prometheus_timeout'),
default=generator.values.get('externalPrometheus', {}).get('timeout', '10s'),
required=False
)
generator.values['externalPrometheus']['timeout'] = timeout
# Configure authentication
if prompt_yes_no(_t('prometheus_auth_required'), default=False):
username = prompt(
_t('prometheus_username'),
default=generator.values.get('externalPrometheus', {}).get('username', ''),
required=False
)
generator.values['externalPrometheus']['username'] = username
password = prompt(
_t('prometheus_password'),
default='',
required=False
)
if password:
generator.values['externalPrometheus']['password'] = password
# Configure insecure (skip TLS verification)
insecure = prompt_yes_no(
_t('prometheus_insecure'),
default=generator.values.get('externalPrometheus', {}).get('insecure', True)
)
generator.values['externalPrometheus']['insecure'] = insecure
if insecure:
print_warning(_t('prometheus_insecure_warning'))
print_success(_t('external_prometheus_configured'))
+102
View File
@@ -0,0 +1,102 @@
"""
Plugin Metric Feature - Available from Chart version 3.7.0+
This feature adds plugin_manager.metric configuration for plugin resource monitoring.
Supports multiple metric sources: disabled, cadvisor, or prometheus.
"""
from .base import Feature, register_feature
from utils import print_section, print_info, print_success, print_warning, prompt, prompt_choice, prompt_yes_no
from i18n import get_translator
@register_feature(
min_version="3.7.0",
module="plugins",
name="Plugin Metrics",
description="Configure plugin resource monitoring (CPU, memory, network I/O)"
)
class PluginMetricFeature(Feature):
"""
Configure plugin_manager.metric settings (3.7.0+)
This feature enables monitoring of plugin resources:
- CPU usage
- Memory usage
- Network I/O
Metric sources:
- disabled: No metrics collected
- cadvisor: Metrics from cadvisor (requires cluster roles)
- prometheus: Query from external Prometheus (recommended)
"""
def configure(self, generator) -> None:
"""Configure plugin metrics settings"""
_t = get_translator()
print_section(_t('plugin_metric_config'))
print_info(_t('plugin_metric_desc'))
# Ensure plugin_manager configuration exists
if 'plugin_manager' not in generator.values:
generator.values['plugin_manager'] = {}
# Check if user wants to configure plugin metrics
if not prompt_yes_no(_t('config_plugin_metric'), default=False):
print_info(_t('using_default_plugin_metric'))
return
# Select metric source
print_info("")
print_info(_t('plugin_metric_source_options'))
print_info(f" • disabled - {_t('plugin_metric_disabled_desc')}")
print_info(f" • cadvisor - {_t('plugin_metric_cadvisor_desc')}")
print_info(f" • prometheus - {_t('plugin_metric_prometheus_desc')}")
metric_source = prompt_choice(
_t('plugin_metric_source'),
["disabled", "cadvisor", "prometheus"],
default=generator.values.get('plugin_manager', {}).get('metric', {}).get('source', 'disabled')
)
# Ensure metric section exists
if 'metric' not in generator.values['plugin_manager']:
generator.values['plugin_manager']['metric'] = {}
generator.values['plugin_manager']['metric']['source'] = metric_source
if metric_source == "cadvisor":
print_warning(_t('cadvisor_cluster_role_warning'))
# Configure scrape settings
if prompt_yes_no(_t('config_cadvisor_scrape'), default=False):
if 'scrape' not in generator.values['plugin_manager']['metric']:
generator.values['plugin_manager']['metric']['scrape'] = {}
scrape_interval = prompt(
_t('scrape_interval'),
default=generator.values['plugin_manager']['metric'].get('scrape', {}).get('scrapeInterval', '20s'),
required=False
)
generator.values['plugin_manager']['metric']['scrape']['scrapeInterval'] = scrape_interval
scrape_timeout = prompt(
_t('scrape_timeout'),
default=generator.values['plugin_manager']['metric'].get('scrape', {}).get('scrapeTimeout', '10s'),
required=False
)
generator.values['plugin_manager']['metric']['scrape']['scrapeTimeout'] = scrape_timeout
retain_period = prompt(
_t('retain_period'),
default=generator.values['plugin_manager']['metric'].get('scrape', {}).get('retainPeriod', '604800s'),
required=False
)
generator.values['plugin_manager']['metric']['scrape']['retainPeriod'] = retain_period
elif metric_source == "prometheus":
print_info(_t('prometheus_external_required'))
print_info(_t('prometheus_config_in_infrastructure'))
print_success(_t('plugin_metric_configured'))
+109
View File
@@ -0,0 +1,109 @@
"""
Trigger Worker Feature - Available from Chart version 3.7.0+
This feature adds the triggerWorker service configuration for workflow trigger functionality.
The triggerWorker is a dedicated Celery worker for handling workflow triggers.
"""
from .base import Feature, register_feature
from utils import print_section, print_info, print_success, prompt, prompt_yes_no
from i18n import get_translator
@register_feature(
min_version="3.7.0",
module="services",
name="Trigger Worker Service",
description="Configure triggerWorker service for workflow trigger processing"
)
class TriggerWorkerFeature(Feature):
"""
Configure triggerWorker service (3.7.0+)
This service is used for:
- Processing workflow webhook triggers
- Handling scheduled workflow executions
- Managing trigger-based workflow invocations
"""
def configure(self, generator) -> None:
"""Configure trigger worker service settings"""
_t = get_translator()
print_section(_t('trigger_worker_config'))
print_info(_t('trigger_worker_desc'))
# Ensure triggerWorker configuration exists
if 'triggerWorker' not in generator.values:
generator.values['triggerWorker'] = {}
# Check if user wants to configure trigger worker
if not prompt_yes_no(_t('config_trigger_worker'), default=False):
print_info(_t('using_default_trigger_worker'))
return
# Configure replicas
replicas_input = prompt(
_t('trigger_worker_replicas'),
default=str(generator.values.get('triggerWorker', {}).get('replicas', 1)),
required=False
)
try:
replicas = int(replicas_input)
if replicas >= 1:
generator.values['triggerWorker']['replicas'] = replicas
except ValueError:
pass
# Configure celery worker amount
celery_input = prompt(
_t('trigger_worker_celery_amount'),
default=str(generator.values.get('triggerWorker', {}).get('celeryWorkerAmount', 1)),
required=False
)
try:
celery_amount = int(celery_input)
if celery_amount >= 1:
generator.values['triggerWorker']['celeryWorkerAmount'] = celery_amount
except ValueError:
pass
# Configure code execution limits
print_info(_t('trigger_worker_code_limits_desc'))
if prompt_yes_no(_t('config_trigger_worker_code_limits'), default=False):
# Ensure code section exists
if 'code' not in generator.values['triggerWorker']:
generator.values['triggerWorker']['code'] = {}
max_string = prompt(
_t('max_string_array_length'),
default=str(generator.values.get('triggerWorker', {}).get('code', {}).get('maxStringArrayLength', 500)),
required=False
)
try:
generator.values['triggerWorker']['code']['maxStringArrayLength'] = int(max_string)
except ValueError:
pass
max_object = prompt(
_t('max_object_array_length'),
default=str(generator.values.get('triggerWorker', {}).get('code', {}).get('maxObjectArrayLength', 500)),
required=False
)
try:
generator.values['triggerWorker']['code']['maxObjectArrayLength'] = int(max_object)
except ValueError:
pass
max_number = prompt(
_t('max_number_array_length'),
default=str(generator.values.get('triggerWorker', {}).get('code', {}).get('maxNumberArrayLength', 500)),
required=False
)
try:
generator.values['triggerWorker']['code']['maxNumberArrayLength'] = int(max_number)
except ValueError:
pass
print_success(_t('trigger_worker_configured'))
+15 -1
View File
@@ -6,6 +6,8 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
from modules.features import apply_features
from modules.features.base import version_satisfies
_t = get_translator()
@@ -74,6 +76,16 @@ def configure_global(generator):
required=False
)
# Trigger domain (3.7.0+)
chart_version = getattr(generator, 'chart_version', None)
if chart_version and version_satisfies(chart_version, "3.7.0"):
print_info(_t('trigger_domain_desc'))
generator.values['global']['triggerDomain'] = prompt(
_t('trigger_domain'),
default="trigger.dify.local",
required=False
)
# Database migration
generator.values['global']['dbMigrationEnabled'] = prompt_yes_no(
_t('enable_db_migration'),
@@ -132,4 +144,6 @@ def configure_global(generator):
except ValueError:
print_warning(f"{_t('invalid_number_use_default')} 4000")
# ==================== 模块 2: 基础设施配置 ====================
# Apply version-specific features for global module
# Features are automatically discovered based on chart_version
apply_features(generator, "global")
+19
View File
@@ -6,6 +6,7 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
from modules.features import apply_features
_t = get_translator()
@@ -556,4 +557,22 @@ def configure_infrastructure(generator):
required=False
)
# Advanced SSRF Proxy configuration
print_section(_t('advanced_config'))
if prompt_yes_no(_t('config_advanced_options'), default=False):
# ssrfProxy.sandboxHost
print_info(_t('ssrf_proxy_sandbox_host_desc'))
sandbox_host = prompt(
_t('ssrf_proxy_sandbox_host'),
default="",
required=False
)
if sandbox_host:
if 'ssrfProxy' not in generator.values:
generator.values['ssrfProxy'] = {}
generator.values['ssrfProxy']['sandboxHost'] = sandbox_host
# Apply version-specific features for infrastructure module
apply_features(generator, "infrastructure")
# ==================== 模块 3: 网络配置 ====================
+5
View File
@@ -6,6 +6,7 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
from modules.features import apply_features
_t = get_translator()
@@ -195,3 +196,7 @@ def configure_plugins(generator):
else:
print_success(_t('https_selected'))
# Apply version-specific features for plugins module
# Features are automatically discovered based on chart_version
apply_features(generator, "plugins")
+5
View File
@@ -6,6 +6,7 @@ from utils import (
)
from version_manager import VersionManager
from i18n import get_translator
from modules.features import apply_features
_t = get_translator()
@@ -95,3 +96,7 @@ def configure_services(generator):
# Note: unstructured.enabled is automatically configured in global_config based on RAG etlType
# No need to configure service enablement here
# Apply version-specific features for services module
# Features are automatically discovered based on chart_version
apply_features(generator, "services")
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""
Test script for version-specific features
This script tests:
1. Version parsing and comparison
2. Feature registration and discovery
3. Feature applicability based on chart version
"""
import sys
from pathlib import Path
# Add project root to path
sys.path.insert(0, str(Path(__file__).parent))
from modules.features.base import (
parse_version,
compare_versions,
version_satisfies,
FeatureRegistry
)
def test_version_parsing():
"""Test version parsing"""
print("\n=== 测试版本解析 ===")
test_cases = [
("3.6.0", (3, 6, 0, 999, 0)),
("3.7.0", (3, 7, 0, 999, 0)),
("3.6.0-beta.1", (3, 6, 0, 2, 1)),
("3.6.0-alpha.2", (3, 6, 0, 1, 2)),
("3.6.0-rc.1", (3, 6, 0, 3, 1)),
]
for version_str, expected in test_cases:
result = parse_version(version_str)
status = "" if result == expected else ""
print(f" {status} parse_version('{version_str}') = {result}")
if result != expected:
print(f" Expected: {expected}")
def test_version_comparison():
"""Test version comparison"""
print("\n=== 测试版本比较 ===")
test_cases = [
("3.6.0", "3.7.0", -1), # 3.6.0 < 3.7.0
("3.7.0", "3.6.0", 1), # 3.7.0 > 3.6.0
("3.7.0", "3.7.0", 0), # 3.7.0 == 3.7.0
("3.6.0-beta.1", "3.6.0", -1), # beta < release
("3.6.0-alpha.1", "3.6.0-beta.1", -1), # alpha < beta
("3.7.0", "3.6.5", 1), # 3.7.0 > 3.6.5
]
for v1, v2, expected in test_cases:
result = compare_versions(v1, v2)
status = "" if result == expected else ""
op = "<" if result < 0 else (">" if result > 0 else "==")
print(f" {status} {v1} {op} {v2}")
def test_version_satisfies():
"""Test version constraint checking"""
print("\n=== 测试版本约束 ===")
test_cases = [
("3.7.0", "3.7.0", None, True), # 3.7.0 >= 3.7.0
("3.6.5", "3.7.0", None, False), # 3.6.5 < 3.7.0
("3.7.2", "3.7.0", None, True), # 3.7.2 >= 3.7.0
("3.8.0", "3.7.0", "3.8.0", True), # 3.7.0 <= 3.8.0 <= 3.8.0
("3.9.0", "3.7.0", "3.8.0", False), # 3.9.0 > 3.8.0
]
for version, min_v, max_v, expected in test_cases:
result = version_satisfies(version, min_v, max_v)
status = "" if result == expected else ""
constraint = f">= {min_v}" + (f", <= {max_v}" if max_v else "")
print(f" {status} {version} satisfies ({constraint}) = {result}")
def test_feature_registry():
"""Test feature registry"""
print("\n=== 测试特性注册表 ===")
# Import features to trigger registration
import modules.features # This auto-discovers features
# Test for different versions
test_versions = ["3.6.0", "3.6.5", "3.7.0", "3.7.2"]
for version in test_versions:
print(f"\n Chart 版本: {version}")
features = FeatureRegistry.get_all_features(version)
if not features:
print(" (无适用特性)")
else:
for module, feature_list in features.items():
for feature in feature_list:
print(f" → [{module}] {feature.name or feature.__class__.__name__}")
def test_feature_order():
"""Test that triggerDomain is configured in domain section"""
print("\n=== 检查 triggerDomain 配置位置 ===")
# Read global_config.py to check where triggerDomain is configured
global_config_path = Path(__file__).parent / "modules" / "global_config.py"
content = global_config_path.read_text()
# Check if triggerDomain is in domain config section
domain_section_start = content.find("# Domain configuration")
domain_section_end = content.find("# Database migration")
# Check if there's any triggerDomain in domain section
domain_section = content[domain_section_start:domain_section_end] if domain_section_start >= 0 and domain_section_end >= 0 else ""
if "triggerDomain" in domain_section:
print(" ✓ triggerDomain 在域名配置区域")
# Check for version guard
if "version_satisfies" in domain_section and "3.7.0" in domain_section:
print(" ✓ triggerDomain 有版本检测 (>= 3.7.0)")
else:
print(" ✗ triggerDomain 缺少版本检测")
else:
print(" ✗ triggerDomain 不在域名配置区域")
print(" → 建议:应该和其他域名一起在域名配置区域中配置")
def test_ssrf_proxy_sandbox_host():
"""Test that ssrfProxy.sandboxHost is configured as advanced option"""
print("\n=== 检查 ssrfProxy.sandboxHost 配置 ===")
# Read infrastructure.py to check sandboxHost configuration
infra_config_path = Path(__file__).parent / "modules" / "infrastructure.py"
content = infra_config_path.read_text()
# Check if sandboxHost is in advanced config section
if "sandboxHost" in content:
print(" ✓ ssrfProxy.sandboxHost 已配置")
if "config_advanced_options" in content:
print(" ✓ 作为高级选项配置")
else:
print(" ✗ 不是高级选项")
else:
print(" ✗ ssrfProxy.sandboxHost 未配置")
# Check i18n translations
translations_path = Path(__file__).parent / "i18n" / "translations.py"
translations_content = translations_path.read_text()
if "ssrf_proxy_sandbox_host" in translations_content:
print(" ✓ i18n 翻译已添加")
else:
print(" ✗ i18n 翻译缺失")
def test_global_config_trigger_domain():
"""Test that triggerDomain is properly shown based on version"""
print("\n=== 测试 triggerDomain 版本检测 ===")
from modules.features.base import version_satisfies
test_cases = [
("3.6.0", False, "3.6.0 不应显示 triggerDomain"),
("3.6.5", False, "3.6.5 不应显示 triggerDomain"),
("3.7.0", True, "3.7.0 应显示 triggerDomain"),
("3.7.2", True, "3.7.2 应显示 triggerDomain"),
("3.8.0", True, "3.8.0 应显示 triggerDomain"),
]
for version, expected, desc in test_cases:
result = version_satisfies(version, "3.7.0")
status = "" if result == expected else ""
print(f" {status} {desc}: {result}")
def test_all_features_for_versions():
"""Comprehensive test of all features across versions"""
print("\n=== 完整版本特性矩阵 ===")
import modules.features
from modules.features.base import FeatureRegistry, version_satisfies
versions = ["3.5.6", "3.6.0", "3.6.5", "3.7.0", "3.7.2"]
# Build feature matrix
print("\n 版本特性支持矩阵:")
print(" " + "-" * 70)
# Header
header = " 特性名称".ljust(35)
for v in versions:
header += f" {v}".center(8)
print(header)
print(" " + "-" * 70)
# Collect unique features across all versions
all_feature_info = {}
for v in versions:
features_dict = FeatureRegistry.get_all_features(v)
for module, feature_list in features_dict.items():
for feature in feature_list:
key = feature.__class__.__name__
if key not in all_feature_info:
all_feature_info[key] = {
'name': getattr(feature, 'name', feature.__class__.__name__),
'min_version': getattr(feature, 'min_version', None),
'max_version': getattr(feature, 'max_version', None),
'module': module
}
# Display matrix
for key, info in all_feature_info.items():
name = f"[{info['module']}] {info['name']}"[:32]
row = f" {name}".ljust(35)
for v in versions:
supported = version_satisfies(v, info['min_version'], info['max_version'])
row += ("".center(8) if supported else " -".center(8))
print(row)
print(" " + "-" * 70)
def main():
print("=" * 60)
print("Dify EE Helm Chart Values Generator - Feature Tests")
print("=" * 60)
test_version_parsing()
test_version_comparison()
test_version_satisfies()
test_feature_registry()
test_feature_order()
test_global_config_trigger_domain()
test_ssrf_proxy_sandbox_host()
test_all_features_for_versions()
print("\n" + "=" * 60)
print("测试完成")
print("=" * 60)
if __name__ == "__main__":
main()