diff --git a/backend/app/config.py b/backend/app/config.py index 88b85c8..501477d 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,8 +1,57 @@ -from pydantic_settings import BaseSettings +import os from typing import Optional +from urllib.parse import urlsplit, urlunsplit + +from pydantic import model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +_DOCKER_SERVICE_ALIASES = {"postgres", "redis", "minio"} + + +def _is_running_in_container() -> bool: + if os.path.exists("/.dockerenv"): + return True + try: + with open("/proc/1/cgroup", "r", encoding="utf-8") as handle: + return "docker" in handle.read() or "containerd" in handle.read() + except OSError: + return False + + +def _normalize_service_host(host: str) -> str: + if _is_running_in_container(): + return host + return "localhost" if host in _DOCKER_SERVICE_ALIASES else host + + +def _normalize_service_url(url: str) -> str: + parsed = urlsplit(url) + if not parsed.hostname: + return url + + normalized_host = _normalize_service_host(parsed.hostname) + if normalized_host == parsed.hostname: + return url + + netloc = normalized_host + if parsed.username: + auth = parsed.username + if parsed.password: + auth = f"{auth}:{parsed.password}" + netloc = f"{auth}@{netloc}" + if parsed.port: + netloc = f"{netloc}:{parsed.port}" + return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)) class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + case_sensitive=False, + extra="ignore", + ) + # Database postgres_db: str = "hartomat" postgres_user: str = "hartomat" @@ -27,6 +76,12 @@ class Settings(BaseSettings): # Redis / Celery redis_url: str = "redis://localhost:6379/0" + @model_validator(mode="after") + def normalize_runtime_hosts(self) -> "Settings": + self.postgres_host = _normalize_service_host(self.postgres_host) + self.redis_url = _normalize_service_url(self.redis_url) + return self + # JWT jwt_secret_key: str = "changeme" jwt_algorithm: str = "HS256" @@ -42,9 +97,4 @@ class Settings(BaseSettings): upload_dir: str = "/app/uploads" max_upload_size_mb: int = 500 - class Config: - env_file = ".env" - case_sensitive = False - - settings = Settings() diff --git a/backend/tests/test_config_runtime_resolution.py b/backend/tests/test_config_runtime_resolution.py new file mode 100644 index 0000000..ccfbb83 --- /dev/null +++ b/backend/tests/test_config_runtime_resolution.py @@ -0,0 +1,36 @@ +from app import config as config_module + + +def test_settings_normalize_docker_aliases_for_host_runtime(monkeypatch): + monkeypatch.setattr(config_module, "_is_running_in_container", lambda: False) + + settings = config_module.Settings( + postgres_host="postgres", + redis_url="redis://redis:6379/0", + ) + + assert settings.postgres_host == "localhost" + assert settings.redis_url == "redis://localhost:6379/0" + assert settings.database_url == "postgresql+asyncpg://hartomat:hartomat@localhost:5432/hartomat" + + +def test_settings_preserve_service_aliases_inside_container(monkeypatch): + monkeypatch.setattr(config_module, "_is_running_in_container", lambda: True) + + settings = config_module.Settings( + postgres_host="postgres", + redis_url="redis://redis:6379/0", + ) + + assert settings.postgres_host == "postgres" + assert settings.redis_url == "redis://redis:6379/0" + + +def test_normalize_service_url_preserves_auth_and_query(monkeypatch): + monkeypatch.setattr(config_module, "_is_running_in_container", lambda: False) + + normalized = config_module._normalize_service_url( + "redis://user:secret@redis:6380/1?ssl_cert_reqs=none" + ) + + assert normalized == "redis://user:secret@localhost:6380/1?ssl_cert_reqs=none" diff --git a/docs/workflows/CURRENT_EXECUTION_BATCH.md b/docs/workflows/CURRENT_EXECUTION_BATCH.md index ba7ada8..f790a6b 100644 --- a/docs/workflows/CURRENT_EXECUTION_BATCH.md +++ b/docs/workflows/CURRENT_EXECUTION_BATCH.md @@ -152,6 +152,14 @@ Ergebnis: ## Letzte Verifikation +- `backend/.venv/bin/pytest backend/tests/test_config_runtime_resolution.py -q` +- Ergebnis: 3 Tests grün; Host-Runtime normalisiert Docker-Service-Aliase (`postgres`, `redis`) außerhalb von Containern nun automatisch auf `localhost`, Container-Runtime bleibt unverändert +- `backend/.venv/bin/pytest backend/tests/domains/test_workflow_runtime_services.py -q -x` +- Ergebnis: 29 Tests grün; Root Cause für den Host-Testfehler war Celery/Redis-Zugriff über Docker-DNS aus dem Host-Kontext, der jetzt zentral im Config-Layer abgefangen wird +- `curl -I -s http://localhost:5173` +- Ergebnis: Frontend antwortet mit `HTTP/1.1 200 OK` +- `curl -s http://localhost:8888/health` +- Ergebnis: Backend antwortet mit `{"status":"ok","service":"hartomat-backend"}` - `python3 scripts/test_render_pipeline.py --workflow-still-smoke --execution-mode shadow` - Ergebnis: Live-Smoke erfolgreich; Shadow-Comparison stabilisiert auf `WARN` mit `mean_pixel_delta=0.000257`, Legacy bleibt dadurch weiterhin authoritative - `./backend/.venv/bin/pytest -q backend/tests/domains/test_workflow_runtime_services.py -k 'resolve_order_line_template_context_uses_exact_template_and_override or resolve_order_line_material_map_prefers_line_override_over_output_override or resolve_order_line_material_map_allows_node_override or prefers_authoritative_scene_manifest_assignments or keeps_legacy_source_name_fallback_without_scene_manifest'`