diff --git a/README.md b/README.md index 7f4699143..298d5c6c8 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ The snippet above will spin up a postgres database in a container. The `get_conn ## Configuration -| Env Variable | Example | Description | +| Env Variable | Default | Description | | ----------------------------------------- | ----------------------------- | ---------------------------------------- | | `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE` | `/var/run/docker.sock` | Path to Docker's socket used by ryuk | | `TESTCONTAINERS_RYUK_PRIVILEGED` | `false` | Run ryuk as a privileged container | -| `TESTCONTAINERS_RYUK_DISABLED` | `false` | Disable ryuk | +| `TESTCONTAINERS_RYUK_DISABLED` | `true` | Disable ryuk | | `RYUK_CONTAINER_IMAGE` | `testcontainers/ryuk:0.5.1` | Custom image for ryuk | diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 1bf9ad4dc..8ced1742e 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -6,5 +6,5 @@ RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.5.1") RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "true" -RYUK_DISABLED: bool = environ.get("TESTCONTAINERS_RYUK_DISABLED", "false") == "true" +RYUK_DISABLED: bool = environ.get("TESTCONTAINERS_RYUK_DISABLED", "true") == "true" RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock") diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index f0da90bb4..969bf0469 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -1,3 +1,4 @@ +import contextlib from platform import system from socket import socket from typing import TYPE_CHECKING, Optional @@ -89,12 +90,24 @@ def stop(self, force=True, delete_volume=True) -> None: self._container.remove(force=force, v=delete_volume) self.get_docker_client().client.close() - def __enter__(self): + def __enter__(self) -> "DockerContainer": return self.start() def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.stop() + def __del__(self) -> None: + """ + __del__ runs when Python attempts to garbage collect the object. + In case of leaky test design, we still attempt to clean up the container. + """ + if not RYUK_DISABLED: + return + + with contextlib.suppress(Exception): + if self._container is not None: + self.stop() + def get_container_host_ip(self) -> str: # infer from docker host host = self.get_docker_client().host() diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 04fdca59a..b59d1c369 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -10,6 +10,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import atexit import functools as ft import ipaddress import os @@ -20,8 +21,11 @@ from typing import Optional, Union import docker +from docker.errors import NotFound from docker.models.containers import Container, ContainerCollection +from requests.exceptions import ConnectionError +from testcontainers.core.config import RYUK_DISABLED from testcontainers.core.labels import SESSION_ID, create_labels from testcontainers.core.utils import default_gateway_ip, inside_container, setup_logger @@ -77,6 +81,8 @@ def run( labels=create_labels(image, labels), **kwargs, ) + if detach and RYUK_DISABLED: + atexit.register(_stop_container, container) return container def find_host_network(self) -> Optional[str]: @@ -193,3 +199,16 @@ def read_tc_properties() -> dict[str, str]: def get_docker_host() -> Optional[str]: return read_tc_properties().get("tc.host") or os.getenv("DOCKER_HOST") + + +def _stop_container(container: Container) -> None: + try: + container.stop() + except NotFound: + # happens when container is already deleted + pass + except ConnectionError: + # happens when the container running the docker engine is already deleted + pass + except Exception as ex: + LOGGER.warning("failed to shut down container %s with image %s: %s", container.id, container.image, ex) diff --git a/core/tests/test_docker_in_docker.py b/core/tests/test_docker_in_docker.py index 6a424884b..44ec85448 100644 --- a/core/tests/test_docker_in_docker.py +++ b/core/tests/test_docker_in_docker.py @@ -32,6 +32,7 @@ def test_wait_for_logs_docker_in_docker(): command="tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock", volumes={"/var/run/docker.sock": {"bind": "/var/run/docker.sock"}}, detach=True, + auto_remove=True, ) not_really_dind.start() @@ -48,7 +49,7 @@ def test_wait_for_logs_docker_in_docker(): assert stdout, "There should be something on stdout" not_really_dind.stop() - not_really_dind.remove() + # not_really_dind.remove() # auto_remove = True def test_dind_inherits_network(): @@ -62,6 +63,7 @@ def test_dind_inherits_network(): command="tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock", volumes={"/var/run/docker.sock": {"bind": "/var/run/docker.sock"}}, detach=True, + auto_remove=True, ) not_really_dind.start() @@ -83,5 +85,5 @@ def test_dind_inherits_network(): assert stdout, "There should be something on stdout" not_really_dind.stop() - not_really_dind.remove() + # not_really_dind.remove() # auto_remove = True custom_network.remove() diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index 32370ffbc..5fe3887e8 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -1,9 +1,12 @@ +import pytest + from testcontainers.core import container from testcontainers.core.container import Reaper from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs +@pytest.mark.skip("see #491") def test_wait_for_reaper(): container = DockerContainer("hello-world").start() wait_for_logs(container, "Hello from Docker!") @@ -17,6 +20,7 @@ def test_wait_for_reaper(): Reaper.delete_instance() +@pytest.mark.skip("see #491") def test_container_without_ryuk(monkeypatch): monkeypatch.setattr(container, "RYUK_DISABLED", True) with DockerContainer("hello-world") as cont: diff --git a/index.rst b/index.rst index 4e0cd54b9..a9bb1193a 100644 --- a/index.rst +++ b/index.rst @@ -97,13 +97,13 @@ Configuration ------------- +-------------------------------------------+-------------------------------+------------------------------------------+ -| Env Variable | Example | Description | +| Env Variable | Default | Description | +===========================================+===============================+==========================================+ | ``TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE`` | ``/var/run/docker.sock`` | Path to Docker's socket used by ryuk | +-------------------------------------------+-------------------------------+------------------------------------------+ | ``TESTCONTAINERS_RYUK_PRIVILEGED`` | ``false`` | Run ryuk as a privileged container | +-------------------------------------------+-------------------------------+------------------------------------------+ -| ``TESTCONTAINERS_RYUK_DISABLED`` | ``false`` | Disable ryuk | +| ``TESTCONTAINERS_RYUK_DISABLED`` | ``true`` | Disable ryuk | +-------------------------------------------+-------------------------------+------------------------------------------+ | ``RYUK_CONTAINER_IMAGE`` | ``testcontainers/ryuk:0.5.1`` | Custom image for ryuk | +-------------------------------------------+-------------------------------+------------------------------------------+