From 6be5f166e903c9a1c618a0a6a1b54d56383f028f Mon Sep 17 00:00:00 2001 From: kavehtoyser Date: Fri, 11 Sep 2026 11:21:52 +0200 Subject: [PATCH] Release 1.5.1 Security patch + logs-viewer fix on top of 1.5.0: - cryptography 46.0.7->50.0.0, pyjwt 2.12.1->2.13.0, python-multipart 0.0.26->0.0.27 (High-severity Dependabot advisories in the manager deps) - logs viewer no longer goes blank over time (per-request aiodocker client, closed when the stream ends; the cached client leaked follow-stream connections) See CHANGES.md. Co-Authored-By: Claude Opus 4.8 --- CHANGES.md | 10 ++++ manager/runtime_connectors.py | 92 +++++++++++++++++++---------------- requirements_manager.txt | 6 +-- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fc6753b..5a60086 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## 1.5.1 + +### Security + +- Bumped manager dependencies to resolve High-severity advisories: `cryptography` (46.0.7 → 50.0.0), `pyjwt` (2.12.1 → 2.13.0), and `python-multipart` (0.0.26 → 0.0.27). + +### Bugfixes + +- The logs viewer no longer goes blank over time. The manager reused a single Docker log client whose connections leaked whenever a live-follow log stream was closed, so eventually every log request would hang and the page rendered empty. Each log request now uses its own client, released when the stream ends. + ## 1.5.0 ### New Features diff --git a/manager/runtime_connectors.py b/manager/runtime_connectors.py index 9e96a6d..972b3b3 100644 --- a/manager/runtime_connectors.py +++ b/manager/runtime_connectors.py @@ -73,13 +73,6 @@ async def service_logs(self, service, tail, follow, since): class LocalDockerConnector(ConnectorBase): CLIENT = docker.from_env() - _AIO_CLIENT = None - - @classmethod - def _get_aio_client(cls): - if cls._AIO_CLIENT is None: - cls._AIO_CLIENT = aiodocker.Docker() - return cls._AIO_CLIENT def __init__(self): # Create our own docker network @@ -401,27 +394,35 @@ async def service_logs( AsyncGenerator[str, None]: Async infinite generator if following, else async finite generator. """ - container = await self._get_aio_client().containers.get( - create_container_name(service.name, service.version) - ) - - kwargs = { - "tail": tail if tail else "all", - "stdout": True, - "stderr": True, - "follow": follow, - } + # Use a fresh aiodocker client per request and close it in `finally`, so + # the underlying connection is always released -- including when the + # browser abandons a follow=True stream (closing the logs page). A + # long-lived cached client leaked those connections until its pool + # exhausted and every log request hung (empty logs page). + client = aiodocker.Docker() + try: + container = await client.containers.get( + create_container_name(service.name, service.version) + ) - if since: - kwargs["since"] = datetime_to_timestamp(since) - logs = container.log(**kwargs) + kwargs = { + "tail": tail if tail else "all", + "stdout": True, + "stderr": True, + "follow": follow, + } + if since: + kwargs["since"] = datetime_to_timestamp(since) + logs = container.log(**kwargs) - if follow: - async for log in logs: - yield log - else: - for log in await logs: - yield log + if follow: + async for log in logs: + yield log + else: + for log in await logs: + yield log + finally: + await client.close() def manager_logs(self, since: datetime) -> str: """Get the manager logs from a certain time @@ -453,24 +454,29 @@ async def manager_logs_stream( else async finite generator. """ manager = self._get_manager_container() - container = await self._get_aio_client().containers.get(manager.id) + # Fresh client per request, closed in `finally` (see service_logs for why). + client = aiodocker.Docker() + try: + container = await client.containers.get(manager.id) - kwargs = { - "tail": tail if tail else "all", - "stdout": True, - "stderr": True, - "follow": follow, - } - if since: - kwargs["since"] = datetime_to_timestamp(since) - logs = container.log(**kwargs) - - if follow: - async for log in logs: - yield log - else: - for log in await logs: - yield log + kwargs = { + "tail": tail if tail else "all", + "stdout": True, + "stderr": True, + "follow": follow, + } + if since: + kwargs["since"] = datetime_to_timestamp(since) + logs = container.log(**kwargs) + + if follow: + async for log in logs: + yield log + else: + for log in await logs: + yield log + finally: + await client.close() RTE_CONN = LocalDockerConnector() diff --git a/requirements_manager.txt b/requirements_manager.txt index d8fad21..70ebcad 100644 --- a/requirements_manager.txt +++ b/requirements_manager.txt @@ -3,12 +3,12 @@ uvicorn==0.44.0 docker==7.1.0 aiodocker==0.26.0 semver==3.0.4 -python-multipart==0.0.26 +python-multipart==0.0.27 toml==0.10.2 sqlalchemy==2.0.49 dash==4.1.0 -pyjwt==2.12.1 +pyjwt==2.13.0 bcrypt==5.0.0 jinja2==3.1.6 cookiecutter==2.7.1 -cryptography==46.0.7 +cryptography==50.0.0