From 19bb94ee471a7eef9c64b62c45ba156a3aa5e5e3 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 1 Mar 2026 20:48:59 +0100 Subject: [PATCH] Fix fail2ban-master path resolution for Docker container In the Docker image, the app source is copied to /app/app/ (not backend/app/), so parents[2] resolved to '/' instead of /app. This left the fail2ban package absent from sys.path, causing every pickle.loads() call on socket responses to raise: ModuleNotFoundError: No module named 'fail2ban' Replace the hardcoded parents[2] with a walk-up search that iterates over all ancestors until it finds a fail2ban-master/ sibling directory. Works correctly in both local dev and Docker without environment-specific path magic. --- backend/app/main.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 27ed8b6..16802bb 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -39,10 +39,33 @@ from app.utils.fail2ban_client import Fail2BanConnectionError, Fail2BanProtocolE # --------------------------------------------------------------------------- # Ensure the bundled fail2ban package is importable from fail2ban-master/ +# +# The directory layout differs between local dev and the Docker image: +# Local: /backend/app/main.py → fail2ban-master at parents[2] +# Docker: /app/app/main.py → fail2ban-master at parents[1] +# Walk up from this file until we find a "fail2ban-master" sibling directory +# so the path resolution is environment-agnostic. # --------------------------------------------------------------------------- -_FAIL2BAN_MASTER: Path = Path(__file__).resolve().parents[2] / "fail2ban-master" -if str(_FAIL2BAN_MASTER) not in sys.path: - sys.path.insert(0, str(_FAIL2BAN_MASTER)) + + +def _find_fail2ban_master() -> Path | None: + """Return the first ``fail2ban-master`` directory found while walking up. + + Returns: + Absolute :class:`~pathlib.Path` to the ``fail2ban-master`` directory, + or ``None`` if no such directory exists among the ancestors. + """ + here = Path(__file__).resolve() + for ancestor in here.parents: + candidate = ancestor / "fail2ban-master" + if candidate.is_dir(): + return candidate + return None + + +_fail2ban_master: Path | None = _find_fail2ban_master() +if _fail2ban_master is not None and str(_fail2ban_master) not in sys.path: + sys.path.insert(0, str(_fail2ban_master)) log: structlog.stdlib.BoundLogger = structlog.get_logger()