Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion lean/components/docker/docker_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,9 +572,14 @@ def _get_docker_client(self):
error = MoreInfoError("Please make sure Docker is installed and running",
"https://www.lean.io/docs/v2/lean-cli/key-concepts/troubleshooting#02-Common-Errors")

client_kwargs = {}
timeout = self._get_docker_client_timeout()
if timeout is not None:
client_kwargs["timeout"] = timeout

try:
from docker import from_env
docker_client = from_env()
docker_client = from_env(**client_kwargs)
except Exception:
raise error

Expand All @@ -586,6 +591,30 @@ def _get_docker_client(self):

return docker_client

def _get_docker_client_timeout(self) -> Optional[int]:
"""Returns the timeout for Docker API calls set through the DOCKER_CLIENT_TIMEOUT environment variable.

This is the variable Docker Compose reads for the same purpose. The Docker SDK's default of 60 seconds
is too short for some Docker hosts, like remote ones or ones under heavy load.

:return: the timeout in seconds, or None if the variable is not set, so the SDK default is used
"""
from os import environ

value = environ.get("DOCKER_CLIENT_TIMEOUT")
if value is None or value.strip() == "":
return None

try:
timeout = int(value)
except ValueError:
timeout = 0

if timeout <= 0:
raise RuntimeError(f"DOCKER_CLIENT_TIMEOUT must be a positive number of seconds, got '{value}'")

return timeout

def _format_source_path(self, path: str) -> str:
"""Formats a source path so Docker knows what it refers to.

Expand Down
43 changes: 43 additions & 0 deletions tests/components/docker/test_docker_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from pathlib import Path
from unittest import mock

import docker
import pytest

from lean.components.docker.docker_manager import DockerManager


Expand Down Expand Up @@ -46,3 +49,43 @@ def test_write_to_file_does_not_let_the_host_shell_expand_the_payload() -> None:
# $type must reach the container untouched (raw JSON: real double quotes, no backslash escaping)
assert '"$type"' in echo_command
assert "\\" not in echo_command


def _get_docker_client_with_env(monkeypatch, value):
if value is None:
monkeypatch.delenv("DOCKER_CLIENT_TIMEOUT", raising=False)
else:
monkeypatch.setenv("DOCKER_CLIENT_TIMEOUT", value)

docker_manager = _create_docker_manager()
# docker is imported at the top of this file: the tests run on a fake filesystem, where importing it
# inside the test would fail
with mock.patch.object(docker, "from_env") as from_env_mock:
from_env_mock.return_value.ping.return_value = True
docker_manager._get_docker_client()

return from_env_mock


def test_get_docker_client_uses_the_sdk_default_timeout_when_docker_client_timeout_is_not_set(monkeypatch) -> None:
from_env_mock = _get_docker_client_with_env(monkeypatch, None)

from_env_mock.assert_called_once_with()


def test_get_docker_client_uses_the_sdk_default_timeout_when_docker_client_timeout_is_empty(monkeypatch) -> None:
from_env_mock = _get_docker_client_with_env(monkeypatch, " ")

from_env_mock.assert_called_once_with()


def test_get_docker_client_passes_docker_client_timeout_to_the_docker_client(monkeypatch) -> None:
from_env_mock = _get_docker_client_with_env(monkeypatch, "300")

from_env_mock.assert_called_once_with(timeout=300)


@pytest.mark.parametrize("value", ["abc", "0", "-5", "1.5"])
def test_get_docker_client_raises_when_docker_client_timeout_is_invalid(monkeypatch, value: str) -> None:
with pytest.raises(RuntimeError, match="DOCKER_CLIENT_TIMEOUT"):
_get_docker_client_with_env(monkeypatch, value)
Loading