mirror of
				https://github.com/bunny-lab-io/Borealis.git
				synced 2025-10-26 15:21:57 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			306 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			306 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """Configuration helpers for the Borealis Engine runtime.
 | |
| 
 | |
| Stage 2 of the migration focuses on lifting the legacy configuration loading
 | |
| behaviour from :mod:`Data.Server.server` into reusable helpers so the Engine
 | |
| start-up path honours the same environment variables, filesystem layout, and
 | |
| logging expectations.  This module documents the supported launch parameters
 | |
| and exposes typed helpers that the application factory consumes.
 | |
| 
 | |
| Launch overview
 | |
| ---------------
 | |
| The Engine can be started via :func:`Data.Engine.bootstrapper.main` or by
 | |
| invoking :func:`Data.Engine.server.create_app` manually.  Configuration is
 | |
| assembled from (in precedence order):
 | |
| 
 | |
| ``config`` mapping overrides provided to :func:`load_runtime_config`,
 | |
| environment variables prefixed with ``BOREALIS_``, and finally built-in
 | |
| defaults that mirror the legacy server runtime.  Key environment variables are
 | |
| 
 | |
| ``BOREALIS_DATABASE_PATH``  path to the SQLite database file.  Defaults to
 | |
| ``<ProjectRoot>/database.db``.
 | |
| ``BOREALIS_CORS_ORIGINS``   comma separated list of allowed origins for CORS.
 | |
| ``BOREALIS_SECRET``         Flask session secret key.
 | |
| ``BOREALIS_COOKIE_*``       Session cookie policies (``SAMESITE``, ``SECURE``,
 | |
|                             ``DOMAIN``).
 | |
| ``BOREALIS_TLS_*``          TLS certificate, private key, and bundle paths.
 | |
| 
 | |
| When TLS values are not provided explicitly the Engine falls back to the
 | |
| certificate helper shipped with the legacy server, ensuring bundling parity.
 | |
| Logs are written to ``Logs/Engine/engine.log`` with daily rotation and
 | |
| errors are additionally duplicated to ``Logs/Engine/error.log`` so the
 | |
| runtime integrates with the platform's logging policy.
 | |
| """
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| import logging
 | |
| import os
 | |
| from dataclasses import asdict, dataclass, field
 | |
| from logging.handlers import TimedRotatingFileHandler
 | |
| from pathlib import Path
 | |
| from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple
 | |
| 
 | |
| try:  # pragma: no-cover - optional dependency during early migration stages.
 | |
|     from Modules.crypto import certificates  # type: ignore
 | |
| except Exception:  # pragma: no-cover - Engine configuration still works without it.
 | |
|     certificates = None  # type: ignore[assignment]
 | |
| 
 | |
| 
 | |
| ENGINE_DIR = Path(__file__).resolve().parent
 | |
| PROJECT_ROOT = ENGINE_DIR.parent.parent
 | |
| DEFAULT_DATABASE_PATH = PROJECT_ROOT / "database.db"
 | |
| LOG_ROOT = PROJECT_ROOT / "Logs" / "Engine"
 | |
| LOG_FILE_PATH = LOG_ROOT / "engine.log"
 | |
| ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log"
 | |
| 
 | |
| 
 | |
| def _ensure_parent(path: Path) -> None:
 | |
|     try:
 | |
|         path.parent.mkdir(parents=True, exist_ok=True)
 | |
|     except Exception:
 | |
|         # Directory creation failure is non-fatal; subsequent file operations
 | |
|         # will surface the issue with clearer context.
 | |
|         pass
 | |
| 
 | |
| 
 | |
| def _resolve_static_folder() -> str:
 | |
|     candidate_roots = [
 | |
|         PROJECT_ROOT / "Engine" / "web-interface",
 | |
|         ENGINE_DIR / "web-interface",
 | |
|         PROJECT_ROOT / "Data" / "Server" / "web-interface",
 | |
|     ]
 | |
| 
 | |
|     candidates = []
 | |
|     for root in candidate_roots:
 | |
|         absolute_root = root.resolve()
 | |
|         candidates.extend(
 | |
|             [
 | |
|                 absolute_root / "build",
 | |
|                 absolute_root / "dist",
 | |
|                 absolute_root,
 | |
|             ]
 | |
|         )
 | |
| 
 | |
|     for candidate in candidates:
 | |
|         if candidate.is_dir():
 | |
|             return str(candidate)
 | |
| 
 | |
|     return str(candidates[0])
 | |
| 
 | |
| 
 | |
| def _parse_origins(raw: Optional[Any]) -> Optional[List[str]]:
 | |
|     if raw is None:
 | |
|         return None
 | |
|     if isinstance(raw, str):
 | |
|         parts = [part.strip() for part in raw.split(",")]
 | |
|     elif isinstance(raw, Sequence):
 | |
|         parts = [str(part).strip() for part in raw]
 | |
|     else:
 | |
|         return None
 | |
|     origins = [part for part in parts if part]
 | |
|     return origins or None
 | |
| 
 | |
| 
 | |
| def _parse_bool(raw: Any, *, default: bool = False) -> bool:
 | |
|     if raw is None:
 | |
|         return default
 | |
|     if isinstance(raw, bool):
 | |
|         return raw
 | |
|     lowered = str(raw).strip().lower()
 | |
|     if lowered in {"1", "true", "yes", "on"}:
 | |
|         return True
 | |
|     if lowered in {"0", "false", "no", "off"}:
 | |
|         return False
 | |
|     return default
 | |
| 
 | |
| 
 | |
| def _discover_tls_material(config: Mapping[str, Any]) -> Sequence[Optional[str]]:
 | |
|     cert_path = config.get("TLS_CERT_PATH") or os.environ.get("BOREALIS_TLS_CERT") or None
 | |
|     key_path = config.get("TLS_KEY_PATH") or os.environ.get("BOREALIS_TLS_KEY") or None
 | |
|     bundle_path = config.get("TLS_BUNDLE_PATH") or os.environ.get("BOREALIS_TLS_BUNDLE") or None
 | |
| 
 | |
|     if certificates and not all([cert_path, key_path, bundle_path]):
 | |
|         try:
 | |
|             auto_cert, auto_key, auto_bundle = certificates.certificate_paths()
 | |
|         except Exception:
 | |
|             auto_cert = auto_key = auto_bundle = None
 | |
|         else:
 | |
|             cert_path = cert_path or auto_cert
 | |
|             key_path = key_path or auto_key
 | |
|             bundle_path = bundle_path or auto_bundle
 | |
| 
 | |
|     if cert_path:
 | |
|         os.environ.setdefault("BOREALIS_TLS_CERT", str(cert_path))
 | |
|     if key_path:
 | |
|         os.environ.setdefault("BOREALIS_TLS_KEY", str(key_path))
 | |
|     if bundle_path:
 | |
|         os.environ.setdefault("BOREALIS_TLS_BUNDLE", str(bundle_path))
 | |
| 
 | |
|     return cert_path, key_path, bundle_path
 | |
| 
 | |
| 
 | |
| @dataclass
 | |
| class EngineSettings:
 | |
|     """Resolved configuration values for the Engine runtime."""
 | |
| 
 | |
|     database_path: str
 | |
|     static_folder: str
 | |
|     cors_origins: Optional[List[str]]
 | |
|     secret_key: str
 | |
|     session_cookie_samesite: str
 | |
|     session_cookie_secure: bool
 | |
|     session_cookie_domain: Optional[str]
 | |
|     tls_cert_path: Optional[str]
 | |
|     tls_key_path: Optional[str]
 | |
|     tls_bundle_path: Optional[str]
 | |
|     log_file: str
 | |
|     error_log_file: str
 | |
|     api_groups: Tuple[str, ...]
 | |
|     raw: MutableMapping[str, Any] = field(default_factory=dict)
 | |
| 
 | |
|     def to_flask_config(self) -> MutableMapping[str, Any]:
 | |
|         config: MutableMapping[str, Any] = {
 | |
|             "SESSION_COOKIE_HTTPONLY": True,
 | |
|             "SESSION_COOKIE_SAMESITE": self.session_cookie_samesite,
 | |
|             "SESSION_COOKIE_SECURE": self.session_cookie_secure,
 | |
|             "PREFERRED_URL_SCHEME": "https",
 | |
|         }
 | |
|         if self.session_cookie_domain:
 | |
|             config["SESSION_COOKIE_DOMAIN"] = self.session_cookie_domain
 | |
|         return config
 | |
| 
 | |
|     def as_dict(self) -> MutableMapping[str, Any]:
 | |
|         data = asdict(self)
 | |
|         data["raw"] = dict(self.raw)
 | |
|         return data
 | |
| 
 | |
| 
 | |
| def _parse_api_groups(raw: Optional[Any]) -> Tuple[str, ...]:
 | |
|     if raw is None:
 | |
|         return tuple()
 | |
|     if isinstance(raw, str):
 | |
|         parts: Iterable[str] = (part.strip() for part in raw.split(","))
 | |
|     elif isinstance(raw, Sequence):
 | |
|         parts = (str(part).strip() for part in raw)
 | |
|     else:
 | |
|         return tuple()
 | |
|     cleaned = [part.lower() for part in parts if part]
 | |
|     return tuple(dict.fromkeys(cleaned))
 | |
| 
 | |
| 
 | |
| def load_runtime_config(overrides: Optional[Mapping[str, Any]] = None) -> EngineSettings:
 | |
|     """Resolve Engine configuration values.
 | |
| 
 | |
|     Parameters
 | |
|     ----------
 | |
|     overrides:
 | |
|         Optional mapping of explicit configuration values.  These take
 | |
|         precedence over environment variables and built-in defaults.
 | |
|     """
 | |
| 
 | |
|     runtime_config: MutableMapping[str, Any] = dict(overrides or {})
 | |
| 
 | |
|     database_path = str(
 | |
|         runtime_config.get("DATABASE_PATH")
 | |
|         or os.environ.get("BOREALIS_DATABASE_PATH")
 | |
|         or DEFAULT_DATABASE_PATH
 | |
|     )
 | |
|     database_path = os.path.abspath(database_path)
 | |
|     _ensure_parent(Path(database_path))
 | |
| 
 | |
|     static_folder = str(runtime_config.get("STATIC_FOLDER") or _resolve_static_folder())
 | |
| 
 | |
|     cors_origins = _parse_origins(
 | |
|         runtime_config.get("CORS_ORIGINS") or os.environ.get("BOREALIS_CORS_ORIGINS")
 | |
|     )
 | |
| 
 | |
|     secret_key = str(runtime_config.get("SECRET_KEY") or os.environ.get("BOREALIS_SECRET") or "borealis-dev-secret")
 | |
| 
 | |
|     session_cookie_samesite = str(
 | |
|         runtime_config.get("SESSION_COOKIE_SAMESITE")
 | |
|         or os.environ.get("BOREALIS_COOKIE_SAMESITE")
 | |
|         or "Lax"
 | |
|     )
 | |
| 
 | |
|     session_cookie_secure = _parse_bool(
 | |
|         runtime_config.get("SESSION_COOKIE_SECURE"),
 | |
|         default=_parse_bool(os.environ.get("BOREALIS_COOKIE_SECURE"), default=False),
 | |
|     )
 | |
| 
 | |
|     session_cookie_domain = runtime_config.get("SESSION_COOKIE_DOMAIN") or os.environ.get("BOREALIS_COOKIE_DOMAIN")
 | |
|     session_cookie_domain = str(session_cookie_domain) if session_cookie_domain else None
 | |
| 
 | |
|     tls_cert_path, tls_key_path, tls_bundle_path = _discover_tls_material(runtime_config)
 | |
| 
 | |
|     log_file = str(runtime_config.get("LOG_FILE") or LOG_FILE_PATH)
 | |
|     _ensure_parent(Path(log_file))
 | |
| 
 | |
|     error_log_file = str(runtime_config.get("ERROR_LOG_FILE") or ERROR_LOG_FILE_PATH)
 | |
|     _ensure_parent(Path(error_log_file))
 | |
| 
 | |
|     api_groups = _parse_api_groups(
 | |
|         runtime_config.get("API_GROUPS") or os.environ.get("BOREALIS_API_GROUPS")
 | |
|     )
 | |
|     if not api_groups:
 | |
|         api_groups = ("tokens", "enrollment")
 | |
| 
 | |
|     settings = EngineSettings(
 | |
|         database_path=database_path,
 | |
|         static_folder=static_folder,
 | |
|         cors_origins=cors_origins,
 | |
|         secret_key=secret_key,
 | |
|         session_cookie_samesite=session_cookie_samesite,
 | |
|         session_cookie_secure=session_cookie_secure,
 | |
|         session_cookie_domain=session_cookie_domain,
 | |
|         tls_cert_path=tls_cert_path if tls_cert_path else None,
 | |
|         tls_key_path=tls_key_path if tls_key_path else None,
 | |
|         tls_bundle_path=tls_bundle_path if tls_bundle_path else None,
 | |
|         log_file=str(log_file),
 | |
|         error_log_file=str(error_log_file),
 | |
|         api_groups=api_groups,
 | |
|         raw=runtime_config,
 | |
|     )
 | |
|     return settings
 | |
| 
 | |
| 
 | |
| def initialise_engine_logger(settings: EngineSettings, name: str = "borealis.engine") -> logging.Logger:
 | |
|     """Configure the Engine logger to write to Engine log files."""
 | |
| 
 | |
|     logger = logging.getLogger(name)
 | |
|     if not logger.handlers:
 | |
|         formatter = logging.Formatter("%(asctime)s-%(name)s-%(levelname)s: %(message)s")
 | |
| 
 | |
|         stream_handler = logging.StreamHandler()
 | |
|         stream_handler.setFormatter(formatter)
 | |
|         logger.addHandler(stream_handler)
 | |
| 
 | |
|         file_handler = TimedRotatingFileHandler(
 | |
|             settings.log_file,
 | |
|             when="midnight",
 | |
|             backupCount=0,
 | |
|             encoding="utf-8",
 | |
|         )
 | |
|         file_handler.setFormatter(formatter)
 | |
|         logger.addHandler(file_handler)
 | |
| 
 | |
|         error_handler = TimedRotatingFileHandler(
 | |
|             settings.error_log_file,
 | |
|             when="midnight",
 | |
|             backupCount=0,
 | |
|             encoding="utf-8",
 | |
|         )
 | |
|         error_handler.setLevel(logging.ERROR)
 | |
|         error_handler.setFormatter(formatter)
 | |
|         logger.addHandler(error_handler)
 | |
| 
 | |
|     logger.setLevel(logging.INFO)
 | |
|     logger.propagate = False
 | |
|     return logger
 | |
| 
 | |
| 
 | |
| __all__ = [
 | |
|     "EngineSettings",
 | |
|     "initialise_engine_logger",
 | |
|     "load_runtime_config",
 | |
| ]
 |