mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 23:44:24 +01:00
clean commit
This commit is contained in:
308
backend/app/services/module_config_manager.py
Normal file
308
backend/app/services/module_config_manager.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
Module-specific configuration management service
|
||||
Works alongside the general ConfigManager for module discovery and schema validation
|
||||
"""
|
||||
import json
|
||||
import yaml
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
from jsonschema import validate, ValidationError, draft7_format_checker
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.utils.exceptions import ConfigurationError
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModuleManifest:
|
||||
"""Module manifest loaded from module.yaml"""
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
author: str
|
||||
category: str = "general"
|
||||
enabled: bool = True
|
||||
auto_start: bool = True
|
||||
dependencies: List[str] = None
|
||||
optional_dependencies: List[str] = None
|
||||
config_schema: Optional[str] = None
|
||||
ui_components: Optional[str] = None
|
||||
provides: List[str] = None
|
||||
consumes: List[str] = None
|
||||
endpoints: List[Dict] = None
|
||||
workflow_steps: List[Dict] = None
|
||||
permissions: List[Dict] = None
|
||||
analytics_events: List[Dict] = None
|
||||
health_checks: List[Dict] = None
|
||||
ui_config: Dict = None
|
||||
documentation: Dict = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.dependencies is None:
|
||||
self.dependencies = []
|
||||
if self.optional_dependencies is None:
|
||||
self.optional_dependencies = []
|
||||
if self.provides is None:
|
||||
self.provides = []
|
||||
if self.consumes is None:
|
||||
self.consumes = []
|
||||
if self.endpoints is None:
|
||||
self.endpoints = []
|
||||
if self.workflow_steps is None:
|
||||
self.workflow_steps = []
|
||||
if self.permissions is None:
|
||||
self.permissions = []
|
||||
if self.analytics_events is None:
|
||||
self.analytics_events = []
|
||||
if self.health_checks is None:
|
||||
self.health_checks = []
|
||||
if self.ui_config is None:
|
||||
self.ui_config = {}
|
||||
if self.documentation is None:
|
||||
self.documentation = {}
|
||||
|
||||
|
||||
class ModuleConfigManager:
|
||||
"""Manages module configurations and JSON schema validation"""
|
||||
|
||||
def __init__(self):
|
||||
self.manifests: Dict[str, ModuleManifest] = {}
|
||||
self.schemas: Dict[str, Dict] = {}
|
||||
self.configs: Dict[str, Dict] = {}
|
||||
|
||||
async def discover_modules(self, modules_path: str = "modules") -> Dict[str, ModuleManifest]:
|
||||
"""Discover modules from filesystem using module.yaml manifests"""
|
||||
discovered_modules = {}
|
||||
|
||||
modules_dir = Path(modules_path)
|
||||
if not modules_dir.exists():
|
||||
logger.warning(f"Modules directory not found: {modules_path}")
|
||||
return discovered_modules
|
||||
|
||||
logger.info(f"Discovering modules in: {modules_dir.absolute()}")
|
||||
|
||||
for module_dir in modules_dir.iterdir():
|
||||
if not module_dir.is_dir():
|
||||
continue
|
||||
|
||||
manifest_path = module_dir / "module.yaml"
|
||||
if not manifest_path.exists():
|
||||
# Try module.yml as fallback
|
||||
manifest_path = module_dir / "module.yml"
|
||||
if not manifest_path.exists():
|
||||
# Check if it's a legacy module (has main.py but no manifest)
|
||||
if (module_dir / "main.py").exists():
|
||||
logger.info(f"Legacy module found (no manifest): {module_dir.name}")
|
||||
# Create a basic manifest for legacy modules
|
||||
manifest = ModuleManifest(
|
||||
name=module_dir.name,
|
||||
version="1.0.0",
|
||||
description=f"Legacy {module_dir.name} module",
|
||||
author="System",
|
||||
category="legacy"
|
||||
)
|
||||
discovered_modules[manifest.name] = manifest
|
||||
continue
|
||||
|
||||
try:
|
||||
manifest = await self._load_module_manifest(manifest_path)
|
||||
discovered_modules[manifest.name] = manifest
|
||||
logger.info(f"Discovered module: {manifest.name} v{manifest.version}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load manifest for {module_dir.name}: {e}")
|
||||
continue
|
||||
|
||||
self.manifests = discovered_modules
|
||||
return discovered_modules
|
||||
|
||||
async def _load_module_manifest(self, manifest_path: Path) -> ModuleManifest:
|
||||
"""Load and validate a module manifest file"""
|
||||
try:
|
||||
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||
manifest_data = yaml.safe_load(f)
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['name', 'version', 'description', 'author']
|
||||
for field in required_fields:
|
||||
if field not in manifest_data:
|
||||
raise ConfigurationError(f"Missing required field '{field}' in {manifest_path}")
|
||||
|
||||
manifest = ModuleManifest(**manifest_data)
|
||||
|
||||
# Load configuration schema if specified
|
||||
if manifest.config_schema:
|
||||
schema_path = manifest_path.parent / manifest.config_schema
|
||||
if schema_path.exists():
|
||||
await self._load_module_schema(manifest.name, schema_path)
|
||||
else:
|
||||
logger.warning(f"Config schema not found: {schema_path}")
|
||||
|
||||
return manifest
|
||||
|
||||
except yaml.YAMLError as e:
|
||||
raise ConfigurationError(f"Invalid YAML in {manifest_path}: {e}")
|
||||
except Exception as e:
|
||||
raise ConfigurationError(f"Failed to load manifest {manifest_path}: {e}")
|
||||
|
||||
async def _load_module_schema(self, module_name: str, schema_path: Path):
|
||||
"""Load JSON schema for module configuration"""
|
||||
try:
|
||||
with open(schema_path, 'r', encoding='utf-8') as f:
|
||||
schema = json.load(f)
|
||||
|
||||
self.schemas[module_name] = schema
|
||||
logger.info(f"Loaded configuration schema for module: {module_name}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
raise ConfigurationError(f"Invalid JSON schema in {schema_path}: {e}")
|
||||
except Exception as e:
|
||||
raise ConfigurationError(f"Failed to load schema {schema_path}: {e}")
|
||||
|
||||
def get_module_manifest(self, module_name: str) -> Optional[ModuleManifest]:
|
||||
"""Get module manifest by name"""
|
||||
return self.manifests.get(module_name)
|
||||
|
||||
def get_module_schema(self, module_name: str) -> Optional[Dict]:
|
||||
"""Get configuration schema for a module"""
|
||||
return self.schemas.get(module_name)
|
||||
|
||||
def get_module_config(self, module_name: str) -> Dict:
|
||||
"""Get current configuration for a module"""
|
||||
return self.configs.get(module_name, {})
|
||||
|
||||
async def validate_config(self, module_name: str, config: Dict) -> Dict:
|
||||
"""Validate module configuration against its schema"""
|
||||
schema = self.schemas.get(module_name)
|
||||
if not schema:
|
||||
logger.info(f"No schema found for module {module_name}, skipping validation")
|
||||
return {"valid": True, "errors": []}
|
||||
|
||||
try:
|
||||
validate(instance=config, schema=schema, format_checker=draft7_format_checker)
|
||||
return {"valid": True, "errors": []}
|
||||
|
||||
except ValidationError as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [{
|
||||
"path": list(e.path),
|
||||
"message": e.message,
|
||||
"invalid_value": e.instance
|
||||
}]
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [{"message": f"Schema validation failed: {str(e)}"}]
|
||||
}
|
||||
|
||||
async def save_module_config(self, module_name: str, config: Dict) -> bool:
|
||||
"""Save module configuration"""
|
||||
# Validate configuration first
|
||||
validation_result = await self.validate_config(module_name, config)
|
||||
if not validation_result["valid"]:
|
||||
error_messages = [error["message"] for error in validation_result["errors"]]
|
||||
raise ConfigurationError(f"Invalid configuration for {module_name}: {', '.join(error_messages)}")
|
||||
|
||||
# Save configuration
|
||||
self.configs[module_name] = config
|
||||
|
||||
# In production, this would persist to database
|
||||
# For now, we'll save to a local JSON file
|
||||
config_dir = Path("backend/storage/module_configs")
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config_file = config_dir / f"{module_name}.json"
|
||||
try:
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
logger.info(f"Saved configuration for module: {module_name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save config for {module_name}: {e}")
|
||||
return False
|
||||
|
||||
async def load_saved_configs(self):
|
||||
"""Load previously saved module configurations"""
|
||||
config_dir = Path("backend/storage/module_configs")
|
||||
if not config_dir.exists():
|
||||
return
|
||||
|
||||
for config_file in config_dir.glob("*.json"):
|
||||
module_name = config_file.stem
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
self.configs[module_name] = config
|
||||
logger.info(f"Loaded saved configuration for module: {module_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load saved config for {module_name}: {e}")
|
||||
|
||||
def list_available_modules(self) -> List[Dict]:
|
||||
"""List all discovered modules with their metadata"""
|
||||
modules = []
|
||||
for name, manifest in self.manifests.items():
|
||||
modules.append({
|
||||
"name": manifest.name,
|
||||
"version": manifest.version,
|
||||
"description": manifest.description,
|
||||
"author": manifest.author,
|
||||
"category": manifest.category,
|
||||
"enabled": manifest.enabled,
|
||||
"dependencies": manifest.dependencies,
|
||||
"provides": manifest.provides,
|
||||
"consumes": manifest.consumes,
|
||||
"has_schema": name in self.schemas,
|
||||
"has_config": name in self.configs,
|
||||
"ui_config": manifest.ui_config
|
||||
})
|
||||
|
||||
return modules
|
||||
|
||||
def get_workflow_steps(self) -> Dict[str, List[Dict]]:
|
||||
"""Get all available workflow steps from modules"""
|
||||
workflow_steps = {}
|
||||
|
||||
for name, manifest in self.manifests.items():
|
||||
if manifest.workflow_steps:
|
||||
workflow_steps[name] = manifest.workflow_steps
|
||||
|
||||
return workflow_steps
|
||||
|
||||
async def update_module_status(self, module_name: str, enabled: bool) -> bool:
|
||||
"""Update module enabled status"""
|
||||
manifest = self.manifests.get(module_name)
|
||||
if not manifest:
|
||||
return False
|
||||
|
||||
manifest.enabled = enabled
|
||||
|
||||
# Update the manifest file
|
||||
modules_dir = Path("modules")
|
||||
manifest_path = modules_dir / module_name / "module.yaml"
|
||||
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
manifest_dict = asdict(manifest)
|
||||
with open(manifest_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(manifest_dict, f, default_flow_style=False)
|
||||
|
||||
logger.info(f"Updated module status: {module_name} enabled={enabled}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update manifest for {module_name}: {e}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Global module config manager instance
|
||||
module_config_manager = ModuleConfigManager()
|
||||
Reference in New Issue
Block a user