diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md index e0f497b..dc77ee7 100644 --- a/docs/FLOWCHART.md +++ b/docs/FLOWCHART.md @@ -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
≥3.7.0, global"] + F2["trigger_worker.py
≥3.7.0, services"] + F3["plugin_metric.py
≥3.7.0, plugins"] + F4["external_prometheus.py
≥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参数
keywordDataSourceType
topKMaxValue
indexingMaxSegmentationTokensLength] - RAGConfig --> End([模块1完成]) + RAGConfig --> ApplyFeatures{应用版本特性} + + ApplyFeatures --> CheckVersion{chart_version
≥ 3.7.0?} + CheckVersion -->|是| TriggerDomain[配置 triggerDomain
工作流 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选择** (互斥) diff --git a/i18n/translations.py b/i18n/translations.py index 2851a12..5b142a3 100644 --- a/i18n/translations.py +++ b/i18n/translations.py @@ -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 )', '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 )', '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 配置成功', + } } diff --git a/modules/features/__init__.py b/modules/features/__init__.py new file mode 100644 index 0000000..bd48e1b --- /dev/null +++ b/modules/features/__init__.py @@ -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() + diff --git a/modules/features/base.py b/modules/features/base.py new file mode 100644 index 0000000..e600cd8 --- /dev/null +++ b/modules/features/base.py @@ -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}") + diff --git a/modules/features/external_prometheus.py b/modules/features/external_prometheus.py new file mode 100644 index 0000000..e6ead4d --- /dev/null +++ b/modules/features/external_prometheus.py @@ -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')) diff --git a/modules/features/plugin_metric.py b/modules/features/plugin_metric.py new file mode 100644 index 0000000..7add483 --- /dev/null +++ b/modules/features/plugin_metric.py @@ -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')) diff --git a/modules/features/trigger_worker.py b/modules/features/trigger_worker.py new file mode 100644 index 0000000..0431616 --- /dev/null +++ b/modules/features/trigger_worker.py @@ -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')) diff --git a/modules/global_config.py b/modules/global_config.py index b2c312e..86f8823 100644 --- a/modules/global_config.py +++ b/modules/global_config.py @@ -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") diff --git a/modules/infrastructure.py b/modules/infrastructure.py index 6e2dcb7..8c2ca55 100644 --- a/modules/infrastructure.py +++ b/modules/infrastructure.py @@ -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: 网络配置 ==================== diff --git a/modules/plugins.py b/modules/plugins.py index a9165d1..e3d404f 100644 --- a/modules/plugins.py +++ b/modules/plugins.py @@ -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") + diff --git a/modules/services.py b/modules/services.py index 09325d6..6593687 100644 --- a/modules/services.py +++ b/modules/services.py @@ -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") diff --git a/test_features.py b/test_features.py new file mode 100644 index 0000000..081115c --- /dev/null +++ b/test_features.py @@ -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() +