From 7c09cccd73b60e22369071611908555ff86f7eee Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sun, 5 Mar 2023 00:01:05 +0100 Subject: [PATCH 01/38] Add .with_ryuk method to enable container cleanup with ryuk --- core/testcontainers/core/container.py | 17 +++++- core/testcontainers/core/docker_client.py | 10 +-- core/testcontainers/core/images.py | 4 ++ core/testcontainers/core/labels.py | 17 ++++++ core/testcontainers/core/reaper.py | 74 +++++++++++++++++++++++ 5 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 core/testcontainers/core/images.py create mode 100644 core/testcontainers/core/labels.py create mode 100644 core/testcontainers/core/reaper.py diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 65acf9be..173c9c4c 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -2,6 +2,9 @@ import os from typing import Iterable, Optional, Tuple + +from .reaper import Reaper, REAPER_IMAGE +from .labels import LABEL_SESSION_ID, SESSION_ID, create_labels from .waiting_utils import wait_container_is_ready from .docker_client import DockerClient from .exceptions import ContainerStartException @@ -26,6 +29,7 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs self.env = {} self.ports = {} self.volumes = {} + self.ryuk = False self.image = image self._docker = DockerClient(**(docker_client_kw or {})) self._container = None @@ -46,6 +50,10 @@ def with_exposed_ports(self, *ports: Iterable[int]) -> 'DockerContainer': self.ports[port] = None return self + def with_ryuk(self, value: bool) -> 'DockerContainer': + self.ryuk = value + return self + def with_kwargs(self, **kwargs) -> 'DockerContainer': self._kwargs = kwargs return self @@ -56,11 +64,14 @@ def maybe_emulate_amd64(self) -> 'DockerContainer': return self def start(self) -> 'DockerContainer': + if self.ryuk and not self.image == REAPER_IMAGE: + logger.debug("Creating Ryuk container") + Reaper.get_instance() logger.info("Pulling image %s", self.image) docker_client = self.get_docker_client() self._container = docker_client.run( self.image, command=self._command, detach=True, environment=self.env, ports=self.ports, - name=self._name, volumes=self.volumes, **self._kwargs + name=self._name, volumes=self.volumes, cleanup_on_exit=not self.ryuk, **self._kwargs ) logger.info("Container started: %s", self._container.short_id) return self @@ -76,9 +87,9 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None: def __del__(self) -> None: """ - Try to remove the container in all circumstances + Try to remove the container if Ryuk is not active """ - if self._container is not None: + if self._container is not None and not self.ryuk: try: self.stop() except: # noqa: E722 diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index e2e4b8b2..33622f0e 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -19,6 +19,8 @@ from typing import List, Optional, Union import urllib + +from .labels import create_labels from .utils import default_gateway_ip, inside_container, setup_logger @@ -44,14 +46,14 @@ def __init__(self, **kwargs) -> None: @ft.wraps(ContainerCollection.run) def run(self, image: str, command: Union[str, List[str]] = None, - environment: Optional[dict] = None, ports: Optional[dict] = None, + environment: Optional[dict] = None, ports: Optional[dict] = None, labels: Optional[dict] = None, detach: bool = False, stdout: bool = True, stderr: bool = False, remove: bool = False, - **kwargs) -> Container: + cleanup_on_exit: bool = True, **kwargs) -> Container: container = self.client.containers.run( image, command=command, stdout=stdout, stderr=stderr, remove=remove, detach=detach, - environment=environment, ports=ports, **kwargs + environment=environment, ports=ports, labels=create_labels(image, labels), **kwargs ) - if detach: + if detach and cleanup_on_exit: atexit.register(_stop_container, container) return container diff --git a/core/testcontainers/core/images.py b/core/testcontainers/core/images.py new file mode 100644 index 00000000..d10766fc --- /dev/null +++ b/core/testcontainers/core/images.py @@ -0,0 +1,4 @@ +from os import environ + + +REAPER_IMAGE = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py new file mode 100644 index 00000000..5282258d --- /dev/null +++ b/core/testcontainers/core/labels.py @@ -0,0 +1,17 @@ +from uuid import uuid4 +from typing import Optional + +from .reaper import REAPER_IMAGE + +SESSION_ID: str = str(uuid4()) +LABEL_SESSION_ID = "org.testcontainers.session-id" + +def create_labels(image: str, labels: Optional[dict]) -> dict: + if labels is None: labels = {} + + if image == REAPER_IMAGE: + return labels + + labels[LABEL_SESSION_ID] = SESSION_ID + return labels + diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py new file mode 100644 index 00000000..ee03f4d4 --- /dev/null +++ b/core/testcontainers/core/reaper.py @@ -0,0 +1,74 @@ +from os import environ +from socket import socket +from typing import TYPE_CHECKING, Optional + + +from .utils import setup_logger +from .images import REAPER_IMAGE +from .waiting_utils import wait_for_logs +from .labels import LABEL_SESSION_ID, SESSION_ID + +if TYPE_CHECKING: + from .container import DockerContainer + + +logger = setup_logger(__name__) + + +class Reaper: + _instance: "Optional[Reaper]" = None + _container: "Optional[DockerContainer]" = None + _socket: Optional[socket] = None + + @classmethod + def get_instance(cls) -> "Reaper": + if not Reaper._instance: + Reaper._instance = Reaper._create_instance() + + return Reaper._instance + + @classmethod + def delete_instance(cls) -> None: + if not Reaper._socket is None: + Reaper._socket.close() + Reaper._socket = None + + if not Reaper._container is None: + Reaper._container.stop() + Reaper._container = None + + if not Reaper._instance is None: + Reaper._instance = None + + + @classmethod + def _create_instance(cls) -> "Reaper": + from .container import DockerContainer + + docker_socket = environ.get( + "TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock" + ) + logger.debug(f"Creating new Reaper for session: {SESSION_ID}") + + Reaper._container = ( + DockerContainer(REAPER_IMAGE) + .with_ryuk(True) + .with_name(f"testcontainers-ryuk-{SESSION_ID}") + .with_exposed_ports(8080) + .with_volume_mapping(docker_socket, "/var/run/docker.sock", "rw") + .start() + ) + wait_for_logs(Reaper._container, r".* Started!") + + container_host = Reaper._container.get_container_host_ip() + container_port = int(Reaper._container.get_exposed_port(8080)) + + Reaper._socket = socket() + Reaper._socket.connect((container_host, container_port)) + Reaper._socket.send(f"label={LABEL_SESSION_ID}={SESSION_ID}\r\n".encode()) + + Reaper._instance = Reaper() + + return Reaper._instance + + From 957b863f14f8ea21d5877222d3f1d8bc6b2c18e5 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sun, 5 Mar 2023 00:01:28 +0100 Subject: [PATCH 02/38] Add tests for ryuk cleanup --- core/tests/test_ryuk.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 core/tests/test_ryuk.py diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py new file mode 100644 index 00000000..02160a80 --- /dev/null +++ b/core/tests/test_ryuk.py @@ -0,0 +1,21 @@ +import pytest + +from testcontainers.core.reaper import Reaper +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs + +def test_wait_for_reaper(): + container = DockerContainer("hello-world").with_ryuk(True).start() + wait_for_logs(container, "Hello from Docker!") + assert Reaper._socket is not None + Reaper._socket.close() + + assert Reaper._container is not None + wait_for_logs(Reaper._container, r".* Removed 1 .*", timeout=15) + + Reaper.delete_instance() + +def test_container_without_ryuk(): + container = DockerContainer("hello-world").start() + wait_for_logs(container, "Hello from Docker!") + assert Reaper._instance is None From e523dba5ca051434ecbfe92f1c426916f3f77269 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sun, 5 Mar 2023 00:15:10 +0100 Subject: [PATCH 03/38] Change param name for clarity --- core/testcontainers/core/container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 173c9c4c..3eb22192 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -50,8 +50,8 @@ def with_exposed_ports(self, *ports: Iterable[int]) -> 'DockerContainer': self.ports[port] = None return self - def with_ryuk(self, value: bool) -> 'DockerContainer': - self.ryuk = value + def with_ryuk(self, enabled: bool) -> 'DockerContainer': + self.ryuk = enabled return self def with_kwargs(self, **kwargs) -> 'DockerContainer': From 543ce3e613600f7bb0bb13fc01759b6f40c292c1 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sun, 5 Mar 2023 00:49:30 +0100 Subject: [PATCH 04/38] Remove unused imports --- core/testcontainers/core/container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 3eb22192..158d3d11 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -4,7 +4,6 @@ from .reaper import Reaper, REAPER_IMAGE -from .labels import LABEL_SESSION_ID, SESSION_ID, create_labels from .waiting_utils import wait_container_is_ready from .docker_client import DockerClient from .exceptions import ContainerStartException From a487dc42954a317a896a8d7c77f0ae2ee43a1dcf Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 6 Mar 2023 13:07:10 +0100 Subject: [PATCH 05/38] Add x-tc-sid header to all Docker APi requests from testcontainers --- core/testcontainers/core/docker_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 33622f0e..22092be7 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -21,6 +21,7 @@ from .labels import create_labels +from .labels import SESSION_ID from .utils import default_gateway_ip, inside_container, setup_logger @@ -43,6 +44,7 @@ class DockerClient: """ def __init__(self, **kwargs) -> None: self.client = docker.from_env(**kwargs) + self.client.api.headers["x-tc-sid"] = SESSION_ID @ft.wraps(ContainerCollection.run) def run(self, image: str, command: Union[str, List[str]] = None, From 38a9d5ae25dde38998672263d0403bbd675c8343 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 6 Mar 2023 13:09:37 +0100 Subject: [PATCH 06/38] Combine imports --- core/testcontainers/core/docker_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 22092be7..d935e505 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -20,8 +20,7 @@ import urllib -from .labels import create_labels -from .labels import SESSION_ID +from .labels import create_labels, SESSION_ID from .utils import default_gateway_ip, inside_container, setup_logger From a214089c1e689994301e1ffdf8c4bf6fbc5d5627 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Wed, 8 Mar 2023 20:50:37 +0100 Subject: [PATCH 07/38] Fix circular import by importing from correct script --- core/testcontainers/core/labels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index 5282258d..65451fa5 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -1,7 +1,7 @@ from uuid import uuid4 from typing import Optional -from .reaper import REAPER_IMAGE +from .images import REAPER_IMAGE SESSION_ID: str = str(uuid4()) LABEL_SESSION_ID = "org.testcontainers.session-id" From fdaee73dafab455659db5eca76ca39edcc1d2522 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Thu, 9 Mar 2023 06:23:12 +0100 Subject: [PATCH 08/38] Fix lint --- core/testcontainers/core/docker_client.py | 8 +++++--- core/testcontainers/core/labels.py | 6 ++++-- core/testcontainers/core/reaper.py | 13 +++++-------- core/tests/test_ryuk.py | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index d935e505..53e2ab05 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -41,15 +41,17 @@ class DockerClient: """ Thin wrapper around :class:`docker.DockerClient` for a more functional interface. """ + def __init__(self, **kwargs) -> None: self.client = docker.from_env(**kwargs) self.client.api.headers["x-tc-sid"] = SESSION_ID @ft.wraps(ContainerCollection.run) def run(self, image: str, command: Union[str, List[str]] = None, - environment: Optional[dict] = None, ports: Optional[dict] = None, labels: Optional[dict] = None, - detach: bool = False, stdout: bool = True, stderr: bool = False, remove: bool = False, - cleanup_on_exit: bool = True, **kwargs) -> Container: + environment: Optional[dict] = None, ports: Optional[dict] = None, + labels: Optional[dict] = None, detach: bool = False, stdout: bool = True, + stderr: bool = False, remove: bool = False, cleanup_on_exit: bool = True, + **kwargs) -> Container: container = self.client.containers.run( image, command=command, stdout=stdout, stderr=stderr, remove=remove, detach=detach, environment=environment, ports=ports, labels=create_labels(image, labels), **kwargs diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index 65451fa5..8ae4ef5f 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -3,15 +3,17 @@ from .images import REAPER_IMAGE + SESSION_ID: str = str(uuid4()) LABEL_SESSION_ID = "org.testcontainers.session-id" + def create_labels(image: str, labels: Optional[dict]) -> dict: - if labels is None: labels = {} + if labels is None: + labels = {} if image == REAPER_IMAGE: return labels labels[LABEL_SESSION_ID] = SESSION_ID return labels - diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index ee03f4d4..8049e210 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -26,20 +26,19 @@ def get_instance(cls) -> "Reaper": Reaper._instance = Reaper._create_instance() return Reaper._instance - + @classmethod def delete_instance(cls) -> None: - if not Reaper._socket is None: + if Reaper._socket is not None: Reaper._socket.close() Reaper._socket = None - - if not Reaper._container is None: + + if Reaper._container is not None: Reaper._container.stop() Reaper._container = None - if not Reaper._instance is None: + if Reaper._instance is not None: Reaper._instance = None - @classmethod def _create_instance(cls) -> "Reaper": @@ -70,5 +69,3 @@ def _create_instance(cls) -> "Reaper": Reaper._instance = Reaper() return Reaper._instance - - diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index 02160a80..398da9eb 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -1,9 +1,8 @@ -import pytest - from testcontainers.core.reaper import Reaper from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs + def test_wait_for_reaper(): container = DockerContainer("hello-world").with_ryuk(True).start() wait_for_logs(container, "Hello from Docker!") @@ -15,6 +14,7 @@ def test_wait_for_reaper(): Reaper.delete_instance() + def test_container_without_ryuk(): container = DockerContainer("hello-world").start() wait_for_logs(container, "Hello from Docker!") From d4ddcbaa1b29b0c9e0a4ed014e847b6cd6414b37 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Thu, 9 Mar 2023 14:16:24 +0100 Subject: [PATCH 09/38] Add testcontainer language implementation to default container labels --- core/testcontainers/core/labels.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index 8ae4ef5f..e32aa84d 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -10,7 +10,9 @@ def create_labels(image: str, labels: Optional[dict]) -> dict: if labels is None: - labels = {} + labels = { + "org.testcontainers.lang": "python", + } if image == REAPER_IMAGE: return labels From 89113a382d230ecdd991b95c97d2d5a2d4ff4b79 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 13:48:50 +0200 Subject: [PATCH 10/38] Rename with_ryuk API to with_auto_remove --- core/testcontainers/core/container.py | 10 +++++----- core/testcontainers/core/docker_client.py | 4 +--- core/testcontainers/core/reaper.py | 1 - 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 158d3d11..5c5c9be1 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -28,7 +28,7 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs self.env = {} self.ports = {} self.volumes = {} - self.ryuk = False + self.auto_remove = True self.image = image self._docker = DockerClient(**(docker_client_kw or {})) self._container = None @@ -49,8 +49,8 @@ def with_exposed_ports(self, *ports: Iterable[int]) -> 'DockerContainer': self.ports[port] = None return self - def with_ryuk(self, enabled: bool) -> 'DockerContainer': - self.ryuk = enabled + def with_auto_remove(self, enabled: bool) -> 'DockerContainer': + self.auto_remove = enabled return self def with_kwargs(self, **kwargs) -> 'DockerContainer': @@ -63,14 +63,14 @@ def maybe_emulate_amd64(self) -> 'DockerContainer': return self def start(self) -> 'DockerContainer': - if self.ryuk and not self.image == REAPER_IMAGE: + if self.auto_remove and not self.image == REAPER_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() logger.info("Pulling image %s", self.image) docker_client = self.get_docker_client() self._container = docker_client.run( self.image, command=self._command, detach=True, environment=self.env, ports=self.ports, - name=self._name, volumes=self.volumes, cleanup_on_exit=not self.ryuk, **self._kwargs + name=self._name, volumes=self.volumes, **self._kwargs ) logger.info("Container started: %s", self._container.short_id) return self diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 809af2f2..db6f5055 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -50,14 +50,12 @@ def __init__(self, **kwargs) -> None: def run(self, image: str, command: Union[str, List[str]] = None, environment: Optional[dict] = None, ports: Optional[dict] = None, labels: Optional[dict] = None, detach: bool = False, stdout: bool = True, - stderr: bool = False, remove: bool = False, cleanup_on_exit: bool = True, + stderr: bool = False, remove: bool = False, **kwargs) -> Container: container = self.client.containers.run( image, command=command, stdout=stdout, stderr=stderr, remove=remove, detach=detach, environment=environment, ports=ports, labels=create_labels(image, labels), **kwargs ) - if detach and cleanup_on_exit: - atexit.register(_stop_container, container) return container def port(self, container_id: str, port: int) -> int: diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index 8049e210..05f0b645 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -51,7 +51,6 @@ def _create_instance(cls) -> "Reaper": Reaper._container = ( DockerContainer(REAPER_IMAGE) - .with_ryuk(True) .with_name(f"testcontainers-ryuk-{SESSION_ID}") .with_exposed_ports(8080) .with_volume_mapping(docker_socket, "/var/run/docker.sock", "rw") From 101fd11e0bd611d6dc2d23fe61f4bee1e652cf90 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 13:55:42 +0200 Subject: [PATCH 11/38] Remove unused imports and function --- core/testcontainers/core/container.py | 10 ---------- core/testcontainers/core/docker_client.py | 12 ------------ core/tests/test_ryuk.py | 20 +++++++++----------- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 5c5c9be1..0b07a7f7 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -84,16 +84,6 @@ def __enter__(self) -> 'DockerContainer': def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.stop() - def __del__(self) -> None: - """ - Try to remove the container if Ryuk is not active - """ - if self._container is not None and not self.ryuk: - try: - self.stop() - except: # noqa: E722 - pass - 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 db6f5055..df85cfa9 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -10,9 +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 docker -from docker.errors import NotFound from docker.models.containers import Container, ContainerCollection import functools as ft import os @@ -27,16 +25,6 @@ LOGGER = setup_logger(__name__) -def _stop_container(container: Container) -> None: - try: - container.stop() - except NotFound: - pass - except Exception as ex: - LOGGER.warning("failed to shut down container %s with image %s: %s", container.id, - container.image, ex) - - class DockerClient: """ Thin wrapper around :class:`docker.DockerClient` for a more functional interface. diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index 398da9eb..06a4885d 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -4,18 +4,16 @@ def test_wait_for_reaper(): - container = DockerContainer("hello-world").with_ryuk(True).start() - wait_for_logs(container, "Hello from Docker!") - assert Reaper._socket is not None - Reaper._socket.close() + with DockerContainer("hello-world") as container: + wait_for_logs(container, "Hello from Docker!") + assert Reaper._socket is not None + Reaper._socket.close() - assert Reaper._container is not None - wait_for_logs(Reaper._container, r".* Removed 1 .*", timeout=15) - - Reaper.delete_instance() + assert Reaper._container is not None + wait_for_logs(Reaper._container, r".* Removed 1 .*", timeout=15) def test_container_without_ryuk(): - container = DockerContainer("hello-world").start() - wait_for_logs(container, "Hello from Docker!") - assert Reaper._instance is None + with DockerContainer("hello-world").with_auto_remove(False) as container: + wait_for_logs(container, "Hello from Docker!") + assert Reaper._instance is None From 8802f39624f1131c6f1b96a70d380e0f9575ea82 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 14:16:43 +0200 Subject: [PATCH 12/38] Update Ryuk tests to not rely on __del__ dunder for cleanup --- core/tests/test_ryuk.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index 06a4885d..eb6f0b38 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -4,13 +4,16 @@ def test_wait_for_reaper(): - with DockerContainer("hello-world") as container: - wait_for_logs(container, "Hello from Docker!") - assert Reaper._socket is not None - Reaper._socket.close() + container = DockerContainer("hello-world").start() + wait_for_logs(container, "Hello from Docker!") + + assert Reaper._socket is not None + Reaper._socket.close() + + assert Reaper._container is not None + wait_for_logs(Reaper._container, r".* Removed 1 .*", timeout=15) - assert Reaper._container is not None - wait_for_logs(Reaper._container, r".* Removed 1 .*", timeout=15) + Reaper.delete_instance() def test_container_without_ryuk(): From 7449d145053f9571ba11f122c5d38940596d1510 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 14:19:01 +0200 Subject: [PATCH 13/38] Update if-statement for better readability --- core/testcontainers/core/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 0b07a7f7..4fcc8bf6 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -63,7 +63,7 @@ def maybe_emulate_amd64(self) -> 'DockerContainer': return self def start(self) -> 'DockerContainer': - if self.auto_remove and not self.image == REAPER_IMAGE: + if self.auto_remove and self.image != REAPER_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() logger.info("Pulling image %s", self.image) From 6c1650d561dfff966024a7b758a7bbc331539bcf Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 14:28:43 +0200 Subject: [PATCH 14/38] Use __future__ annotations to avoid quotation marks --- core/testcontainers/core/reaper.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index 05f0b645..b7d6ae3f 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from os import environ from socket import socket from typing import TYPE_CHECKING, Optional @@ -16,12 +18,12 @@ class Reaper: - _instance: "Optional[Reaper]" = None + _instance: Optional[Reaper] = None _container: "Optional[DockerContainer]" = None _socket: Optional[socket] = None @classmethod - def get_instance(cls) -> "Reaper": + def get_instance(cls) -> Reaper: if not Reaper._instance: Reaper._instance = Reaper._create_instance() @@ -41,7 +43,7 @@ def delete_instance(cls) -> None: Reaper._instance = None @classmethod - def _create_instance(cls) -> "Reaper": + def _create_instance(cls) -> Reaper: from .container import DockerContainer docker_socket = environ.get( From 3e2c66c44d245e9b53e98a149aacfb9e8f652800 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 15:34:46 +0200 Subject: [PATCH 15/38] Move Ryuk container image setting into config.py --- core/testcontainers/core/config.py | 2 ++ core/testcontainers/core/images.py | 4 ---- core/testcontainers/core/labels.py | 2 +- core/testcontainers/core/reaper.py | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 core/testcontainers/core/images.py diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index e7673f75..6f2ee6b6 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -3,3 +3,5 @@ MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120)) SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1)) TIMEOUT = MAX_TRIES * SLEEP_TIME + +REAPER_IMAGE = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") diff --git a/core/testcontainers/core/images.py b/core/testcontainers/core/images.py deleted file mode 100644 index d10766fc..00000000 --- a/core/testcontainers/core/images.py +++ /dev/null @@ -1,4 +0,0 @@ -from os import environ - - -REAPER_IMAGE = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index e32aa84d..8c356123 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -1,7 +1,7 @@ from uuid import uuid4 from typing import Optional -from .images import REAPER_IMAGE +from .config import REAPER_IMAGE SESSION_ID: str = str(uuid4()) diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index b7d6ae3f..30d4fe71 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -6,7 +6,7 @@ from .utils import setup_logger -from .images import REAPER_IMAGE +from .config import REAPER_IMAGE from .waiting_utils import wait_for_logs from .labels import LABEL_SESSION_ID, SESSION_ID From c401df7db38faf759821be89e6f40d0ee73c67de Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 15:54:01 +0200 Subject: [PATCH 16/38] Move all Ryuk config-from-env logic to config.py. Rename to RYUK_ prefix --- core/testcontainers/core/config.py | 4 +++- core/testcontainers/core/container.py | 5 +++-- core/testcontainers/core/labels.py | 4 ++-- core/testcontainers/core/reaper.py | 11 ++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 6f2ee6b6..f909610f 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -4,4 +4,6 @@ SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1)) TIMEOUT = MAX_TRIES * SLEEP_TIME -REAPER_IMAGE = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") +RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") +RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "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 4fcc8bf6..238d959d 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -3,7 +3,8 @@ from typing import Iterable, Optional, Tuple -from .reaper import Reaper, REAPER_IMAGE +from .reaper import Reaper +from .config import RYUK_IMAGE from .waiting_utils import wait_container_is_ready from .docker_client import DockerClient from .exceptions import ContainerStartException @@ -63,7 +64,7 @@ def maybe_emulate_amd64(self) -> 'DockerContainer': return self def start(self) -> 'DockerContainer': - if self.auto_remove and self.image != REAPER_IMAGE: + if self.auto_remove and self.image != RYUK_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() logger.info("Pulling image %s", self.image) diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index 8c356123..fb2e0366 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -1,7 +1,7 @@ from uuid import uuid4 from typing import Optional -from .config import REAPER_IMAGE +from .config import RYUK_IMAGE SESSION_ID: str = str(uuid4()) @@ -14,7 +14,7 @@ def create_labels(image: str, labels: Optional[dict]) -> dict: "org.testcontainers.lang": "python", } - if image == REAPER_IMAGE: + if image == RYUK_IMAGE: return labels labels[LABEL_SESSION_ID] = SESSION_ID diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index 30d4fe71..b0431d4b 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -1,12 +1,11 @@ from __future__ import annotations -from os import environ from socket import socket from typing import TYPE_CHECKING, Optional from .utils import setup_logger -from .config import REAPER_IMAGE +from .config import RYUK_IMAGE, RYUK_DOCKER_SOCKET, RYUK_PRIVILEGED from .waiting_utils import wait_for_logs from .labels import LABEL_SESSION_ID, SESSION_ID @@ -46,16 +45,14 @@ def delete_instance(cls) -> None: def _create_instance(cls) -> Reaper: from .container import DockerContainer - docker_socket = environ.get( - "TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock" - ) logger.debug(f"Creating new Reaper for session: {SESSION_ID}") Reaper._container = ( - DockerContainer(REAPER_IMAGE) + DockerContainer(RYUK_IMAGE) .with_name(f"testcontainers-ryuk-{SESSION_ID}") .with_exposed_ports(8080) - .with_volume_mapping(docker_socket, "/var/run/docker.sock", "rw") + .with_volume_mapping(RYUK_DOCKER_SOCKET, "/var/run/docker.sock", "rw") + .with_kwargs(privileged=RYUK_PRIVILEGED) .start() ) wait_for_logs(Reaper._container, r".* Started!") From 581e38bc8b3ee23bb24862d4c9014a37cf612cb4 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 16:00:31 +0200 Subject: [PATCH 17/38] Fix lint --- core/testcontainers/core/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index f909610f..759f1ab0 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -6,4 +6,5 @@ RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "true" -RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock") +RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", + "/var/run/docker.sock") From 5b1988e216a27cd7ee7c883ec5d7b037947524cc Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 23 May 2023 21:10:38 +0200 Subject: [PATCH 18/38] Fix bug where language label was not added when provided custom labels --- core/testcontainers/core/labels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index fb2e0366..add2d233 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -6,13 +6,13 @@ SESSION_ID: str = str(uuid4()) LABEL_SESSION_ID = "org.testcontainers.session-id" +LABEL_LANG = "org.testcontainers.lang" def create_labels(image: str, labels: Optional[dict]) -> dict: if labels is None: - labels = { - "org.testcontainers.lang": "python", - } + labels = {} + labels[LABEL_LANG] = "python" if image == RYUK_IMAGE: return labels From 6c24543efe6b8b19af3d78e85f16742c88f869ad Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Wed, 24 May 2023 13:52:59 +0200 Subject: [PATCH 19/38] Bump Ryuk container version Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- core/testcontainers/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 759f1ab0..e9a44caa 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -4,7 +4,7 @@ SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1)) TIMEOUT = MAX_TRIES * SLEEP_TIME -RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.3.4") +RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.5.1") RYUK_PRIVILEGED: bool = environ.get("TESTCONTAINERS_RYUK_PRIVILEGED", "false") == "true" RYUK_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/var/run/docker.sock") From 4c37960764b74a57dceec54440dadee5b9dc92b1 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 28 Aug 2023 21:18:14 +0200 Subject: [PATCH 20/38] Add env variables to Docs. Add env variable for disabling Ryuk --- README.rst | 15 +++++++++++++++ core/testcontainers/core/config.py | 1 + core/testcontainers/core/container.py | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f490e76b..3555c65e 100644 --- a/README.rst +++ b/README.rst @@ -70,6 +70,21 @@ When trying to launch a testcontainer from within a Docker container, e.g., in c 1. The container has to provide a docker client installation. Either use an image that has docker pre-installed (e.g. the `official docker images `_) or install the client from within the `Dockerfile` specification. 2. The container has to have access to the docker daemon which can be achieved by mounting `/var/run/docker.sock` or setting the `DOCKER_HOST` environment variable as part of your `docker run` command. +Configuration +------------- + ++-------------------------------------------+-------------------------------+------------------------------------------+ +| Env Variable | Example | 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 | ++-------------------------------------------+-------------------------------+------------------------------------------+ +| ``RYUK_CONTAINER_IMAGE`` | ``testcontainers/ryuk:0.5.1`` | Custom image for ryuk | ++-------------------------------------------+-------------------------------+------------------------------------------+ + Development and Contributing ---------------------------- diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index e9a44caa..4ac09cdc 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -6,5 +6,6 @@ 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_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 238d959d..4de931ef 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -4,7 +4,7 @@ from .reaper import Reaper -from .config import RYUK_IMAGE +from .config import RYUK_IMAGE, RYUK_DISABLED from .waiting_utils import wait_container_is_ready from .docker_client import DockerClient from .exceptions import ContainerStartException @@ -64,7 +64,7 @@ def maybe_emulate_amd64(self) -> 'DockerContainer': return self def start(self) -> 'DockerContainer': - if self.auto_remove and self.image != RYUK_IMAGE: + if not RYUK_DISABLED and self.auto_remove and self.image != RYUK_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() logger.info("Pulling image %s", self.image) From dc66658374d9dd446b754ca2988a9c2d4d682839 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 28 Aug 2023 21:56:33 +0200 Subject: [PATCH 21/38] Replace programmatic .with_auto_remove() API with env variable for disabling Ryuk --- core/testcontainers/core/container.py | 7 +------ core/tests/test_ryuk.py | 8 +++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 4de931ef..a29bead6 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -29,7 +29,6 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs self.env = {} self.ports = {} self.volumes = {} - self.auto_remove = True self.image = image self._docker = DockerClient(**(docker_client_kw or {})) self._container = None @@ -50,10 +49,6 @@ def with_exposed_ports(self, *ports: Iterable[int]) -> 'DockerContainer': self.ports[port] = None return self - def with_auto_remove(self, enabled: bool) -> 'DockerContainer': - self.auto_remove = enabled - return self - def with_kwargs(self, **kwargs) -> 'DockerContainer': self._kwargs = kwargs return self @@ -64,7 +59,7 @@ def maybe_emulate_amd64(self) -> 'DockerContainer': return self def start(self) -> 'DockerContainer': - if not RYUK_DISABLED and self.auto_remove and self.image != RYUK_IMAGE: + if not RYUK_DISABLED and self.image != RYUK_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() logger.info("Pulling image %s", self.image) diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index eb6f0b38..da5046f4 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -1,3 +1,4 @@ +from testcontainers.core import container from testcontainers.core.reaper import Reaper from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs @@ -16,7 +17,8 @@ def test_wait_for_reaper(): Reaper.delete_instance() -def test_container_without_ryuk(): - with DockerContainer("hello-world").with_auto_remove(False) as container: - wait_for_logs(container, "Hello from Docker!") +def test_container_without_ryuk(monkeypatch): + monkeypatch.setattr(container, "RYUK_DISABLED", True) + with DockerContainer("hello-world") as cont: + wait_for_logs(cont, "Hello from Docker!") assert Reaper._instance is None From dd18c01f1ed60b54a1988659400b84b087fa9c31 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 28 Aug 2023 22:05:46 +0200 Subject: [PATCH 22/38] Downgrade PyYAML to 5.3.1 to fix Cython build problem --- requirements/macos-latest-3.10.txt | 2 +- requirements/ubuntu-latest-3.10.txt | 2 +- requirements/ubuntu-latest-3.11.txt | 2 +- requirements/ubuntu-latest-3.7.txt | 2 +- requirements/ubuntu-latest-3.8.txt | 2 +- requirements/ubuntu-latest-3.9.txt | 2 +- requirements/windows-latest-3.10.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/macos-latest-3.10.txt b/requirements/macos-latest-3.10.txt index bffd3237..65b02f55 100644 --- a/requirements/macos-latest-3.10.txt +++ b/requirements/macos-latest-3.10.txt @@ -317,7 +317,7 @@ pytz==2023.3 # via # clickhouse-driver # neo4j -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine diff --git a/requirements/ubuntu-latest-3.10.txt b/requirements/ubuntu-latest-3.10.txt index 5a06928f..0c5d85e3 100644 --- a/requirements/ubuntu-latest-3.10.txt +++ b/requirements/ubuntu-latest-3.10.txt @@ -322,7 +322,7 @@ pytz==2023.3 # via # clickhouse-driver # neo4j -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine diff --git a/requirements/ubuntu-latest-3.11.txt b/requirements/ubuntu-latest-3.11.txt index 107b6742..69be7998 100644 --- a/requirements/ubuntu-latest-3.11.txt +++ b/requirements/ubuntu-latest-3.11.txt @@ -317,7 +317,7 @@ pytz==2023.3 # via # clickhouse-driver # neo4j -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine diff --git a/requirements/ubuntu-latest-3.7.txt b/requirements/ubuntu-latest-3.7.txt index b7197ef2..d2c6de0b 100644 --- a/requirements/ubuntu-latest-3.7.txt +++ b/requirements/ubuntu-latest-3.7.txt @@ -337,7 +337,7 @@ pytz==2023.3 # babel # clickhouse-driver # neo4j -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine diff --git a/requirements/ubuntu-latest-3.8.txt b/requirements/ubuntu-latest-3.8.txt index 58cabaab..f0150901 100644 --- a/requirements/ubuntu-latest-3.8.txt +++ b/requirements/ubuntu-latest-3.8.txt @@ -328,7 +328,7 @@ pytz==2023.3 # babel # clickhouse-driver # neo4j -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine diff --git a/requirements/ubuntu-latest-3.9.txt b/requirements/ubuntu-latest-3.9.txt index bf3410c4..ea9d42d8 100644 --- a/requirements/ubuntu-latest-3.9.txt +++ b/requirements/ubuntu-latest-3.9.txt @@ -323,7 +323,7 @@ pytz==2023.3 # via # clickhouse-driver # neo4j -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine diff --git a/requirements/windows-latest-3.10.txt b/requirements/windows-latest-3.10.txt index 4c6dd2f8..96fa1403 100644 --- a/requirements/windows-latest-3.10.txt +++ b/requirements/windows-latest-3.10.txt @@ -327,7 +327,7 @@ pywin32==306 # via docker pywin32-ctypes==0.2.0 # via keyring -pyyaml==5.4.1 +pyyaml==5.3.1 # via docker-compose readme-renderer==37.3 # via twine From 1e2f7a113815e046bce73ebefe5020172a5edae4 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 28 Aug 2023 22:41:47 +0200 Subject: [PATCH 23/38] Add dependency restriction to testcontainers-compose to mitigate Cython3 build breaking --- compose/setup.py | 1 + requirements/macos-latest-3.10.txt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/compose/setup.py b/compose/setup.py index bfe128e2..1fee06f0 100644 --- a/compose/setup.py +++ b/compose/setup.py @@ -13,6 +13,7 @@ install_requires=[ "testcontainers-core", "docker-compose", + "pyyaml<5.4.0", ], python_requires=">=3.7", ) diff --git a/requirements/macos-latest-3.10.txt b/requirements/macos-latest-3.10.txt index 65b02f55..a92dee1e 100644 --- a/requirements/macos-latest-3.10.txt +++ b/requirements/macos-latest-3.10.txt @@ -318,7 +318,9 @@ pytz==2023.3 # clickhouse-driver # neo4j pyyaml==5.3.1 - # via docker-compose + # via + # docker-compose + # testcontainers-compose readme-renderer==37.3 # via twine redis==4.5.5 From a4827f23338c0d7e975b311d9f362b0136447e32 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 28 Aug 2023 23:20:02 +0200 Subject: [PATCH 24/38] Allow any single digit of containers to be killed. Some may be dangling from failing tests --- core/tests/test_ryuk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index da5046f4..48373fb8 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -12,7 +12,7 @@ def test_wait_for_reaper(): Reaper._socket.close() assert Reaper._container is not None - wait_for_logs(Reaper._container, r".* Removed 1 .*", timeout=15) + wait_for_logs(Reaper._container, r".* Removed \d .*", timeout=30) Reaper.delete_instance() From 07d48491bc6f55217228a2f8d93f6b11fbc3ba5b Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sat, 17 Feb 2024 18:51:09 +0100 Subject: [PATCH 25/38] Remove deprecated setup file --- compose/setup.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 compose/setup.py diff --git a/compose/setup.py b/compose/setup.py deleted file mode 100644 index 1fee06f0..00000000 --- a/compose/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import setup, find_namespace_packages - -description = "Docker Compose component of testcontainers-python." - -setup( - name="testcontainers-compose", - version="0.0.1rc1", - packages=find_namespace_packages(), - description=description, - long_description=description, - long_description_content_type="text/x-rst", - url="https://github.com/testcontainers/testcontainers-python", - install_requires=[ - "testcontainers-core", - "docker-compose", - "pyyaml<5.4.0", - ], - python_requires=">=3.7", -) From c239d523a693706e6affb0ae278cbd9d8bb74b58 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sat, 17 Feb 2024 18:51:19 +0100 Subject: [PATCH 26/38] Use absolute imports --- core/testcontainers/core/docker_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index e5f2c7bb..e934f6ab 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -15,13 +15,12 @@ import urllib from typing import List, Optional, Union +from testcontainers.core.labels import create_labels, SESSION_ID +from testcontainers.core.utils import default_gateway_ip, inside_container, setup_logger + import docker from docker.models.containers import Container, ContainerCollection - -from .labels import create_labels, SESSION_ID -from .utils import default_gateway_ip, inside_container, setup_logger - LOGGER = setup_logger(__name__) From 22e03ebed98eaf6331ef341eb8e07aa0e4de750a Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Sat, 17 Feb 2024 19:20:30 +0100 Subject: [PATCH 27/38] Fix flake8 lint error --- core/testcontainers/core/docker_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index e934f6ab..d5cb322e 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -44,7 +44,7 @@ def run(self, image: str, stderr: bool = False, remove: bool = False, **kwargs - ) -> Container: + ) -> Container: container = self.client.containers.run( image, command=command, stdout=stdout, stderr=stderr, remove=remove, detach=detach, environment=environment, ports=ports, labels=create_labels(image, labels), **kwargs From fed8d49c7fd23c199a89a6d44e0ee1e9ae00eaf8 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 13:11:21 +0100 Subject: [PATCH 28/38] Lint --- core/testcontainers/core/config.py | 3 +-- core/testcontainers/core/container.py | 6 +++--- core/testcontainers/core/docker_client.py | 21 ++++++--------------- core/testcontainers/core/labels.py | 5 ++--- core/testcontainers/core/reaper.py | 13 ++++++------- 5 files changed, 18 insertions(+), 30 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 4ac09cdc..1bf9ad4d 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -7,5 +7,4 @@ 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_DOCKER_SOCKET: str = environ.get("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", - "/var/run/docker.sock") +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 545e7819..1f0f3d8a 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -3,11 +3,11 @@ from docker.models.containers import Container -from testcontainers.core.reaper import Reaper -from testcontainers.core.config import RYUK_IMAGE, RYUK_DISABLED +from testcontainers.core.config import RYUK_DISABLED, RYUK_IMAGE from testcontainers.core.docker_client import DockerClient from testcontainers.core.exceptions import ContainerStartException -from testcontainers.core.utils import setup_logger, inside_container, is_arm +from testcontainers.core.reaper import Reaper +from testcontainers.core.utils import inside_container, is_arm, setup_logger from testcontainers.core.waiting_utils import wait_container_is_ready logger = setup_logger(__name__) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 815e310e..a74c5d05 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -17,12 +17,12 @@ from pathlib import Path from typing import Optional, Union -from testcontainers.core.labels import create_labels, SESSION_ID -from testcontainers.core.utils import default_gateway_ip, inside_container, setup_logger - import docker from docker.models.containers import Container, ContainerCollection +from testcontainers.core.labels import SESSION_ID, create_labels +from testcontainers.core.utils import default_gateway_ip, inside_container, setup_logger + LOGGER = setup_logger(__name__) TC_FILE = ".testcontainers.properties" TC_GLOBAL = Path.home() / TC_FILE @@ -50,12 +50,12 @@ def run( command: Optional[Union[str, list[str]]] = None, environment: Optional[dict] = None, ports: Optional[dict] = None, - labels: Optional[dict] = None, + labels: Optional[dict[str, str]] = None, detach: bool = False, stdout: bool = True, stderr: bool = False, remove: bool = False, - **kwargs + **kwargs, ) -> Container: container = self.client.containers.run( image, @@ -67,7 +67,7 @@ def run( environment=environment, ports=ports, labels=create_labels(image, labels), - **kwargs + **kwargs, ) return container @@ -145,12 +145,3 @@ def read_tc_properties() -> dict[str, str]: tuples = [line.split("=") for line in contents.readlines() if "=" in line] settings = {**settings, **{item[0]: item[1] for item in tuples}} return settings - - -def _stop_container(container: Container) -> None: - try: - container.stop() - except NotFound: - 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/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index 91dd971c..13937a5e 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -1,15 +1,14 @@ -from uuid import uuid4 from typing import Optional +from uuid import uuid4 from testcontainers.core.config import RYUK_IMAGE - SESSION_ID: str = str(uuid4()) LABEL_SESSION_ID = "org.testcontainers.session-id" LABEL_LANG = "org.testcontainers.lang" -def create_labels(image: str, labels: Optional[dict]) -> dict: +def create_labels(image: str, labels: Optional[dict[str, str]]) -> dict[str, str]: if labels is None: labels = {} labels[LABEL_LANG] = "python" diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index b0431d4b..a0759c5b 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -1,13 +1,12 @@ from __future__ import annotations from socket import socket -from typing import TYPE_CHECKING, Optional - +from typing import TYPE_CHECKING, Union +from .config import RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED +from .labels import LABEL_SESSION_ID, SESSION_ID from .utils import setup_logger -from .config import RYUK_IMAGE, RYUK_DOCKER_SOCKET, RYUK_PRIVILEGED from .waiting_utils import wait_for_logs -from .labels import LABEL_SESSION_ID, SESSION_ID if TYPE_CHECKING: from .container import DockerContainer @@ -17,9 +16,9 @@ class Reaper: - _instance: Optional[Reaper] = None - _container: "Optional[DockerContainer]" = None - _socket: Optional[socket] = None + _instance: Union[Reaper, None] = None + _container: "Union[DockerContainer, None]" = None # noqa: UP037 Use quotes for type annotation due to circular dependency on DockerContainer + _socket: Union[socket, None] = None @classmethod def get_instance(cls) -> Reaper: From a5109832aa1aec63e5154c2a2de77ffbe764424c Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 13:11:52 +0100 Subject: [PATCH 29/38] Add comment on why quotes are used for type annotation --- core/testcontainers/core/reaper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index a0759c5b..b5615a2c 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -17,7 +17,9 @@ class Reaper: _instance: Union[Reaper, None] = None - _container: "Union[DockerContainer, None]" = None # noqa: UP037 Use quotes for type annotation due to circular dependency on DockerContainer + _container: "Union[DockerContainer, None]" = ( + None # noqa: UP037 Use quotes for type annotation due to circular dependency on DockerContainer + ) _socket: Union[socket, None] = None @classmethod From 51ccbc43765cea9f68507d6cce6e788a1e0797a7 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 13:13:26 +0100 Subject: [PATCH 30/38] Shorter comment do make linter happy --- core/testcontainers/core/reaper.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index b5615a2c..cf18d7d8 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -17,9 +17,7 @@ class Reaper: _instance: Union[Reaper, None] = None - _container: "Union[DockerContainer, None]" = ( - None # noqa: UP037 Use quotes for type annotation due to circular dependency on DockerContainer - ) + _container: "Union[DockerContainer, None]" = None # noqa: UP037 Use quotes for type annotation due to circular deps _socket: Union[socket, None] = None @classmethod From a0d8487c564831d790e09525bedc5c79c5db4321 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 13:18:53 +0100 Subject: [PATCH 31/38] Disable ruff rule for prefering X | Y over Optional[X] to support Python3.9 --- core/testcontainers/core/reaper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index cf18d7d8..bd1a6cca 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -1,7 +1,7 @@ from __future__ import annotations from socket import socket -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Optional from .config import RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED from .labels import LABEL_SESSION_ID, SESSION_ID @@ -16,9 +16,9 @@ class Reaper: - _instance: Union[Reaper, None] = None - _container: "Union[DockerContainer, None]" = None # noqa: UP037 Use quotes for type annotation due to circular deps - _socket: Union[socket, None] = None + _instance: Optional[Reaper] = None # noqa: UP007 Support Python3.9 type annotations + _container: "Optional[DockerContainer]" = None # noqa: UP037,UP007 Quotes for type annotation due to circular deps + _socket: Optional[socket] = None # noqa: UP007 Support Python3.9 type annotations @classmethod def get_instance(cls) -> Reaper: From e9974b3ba6afa7738f00ac6bf270e66797d8cf34 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 13:42:43 +0100 Subject: [PATCH 32/38] Use absolute imports --- core/testcontainers/core/reaper.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py index bd1a6cca..82123d7f 100644 --- a/core/testcontainers/core/reaper.py +++ b/core/testcontainers/core/reaper.py @@ -3,13 +3,13 @@ from socket import socket from typing import TYPE_CHECKING, Optional -from .config import RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED -from .labels import LABEL_SESSION_ID, SESSION_ID -from .utils import setup_logger -from .waiting_utils import wait_for_logs +from testcontainers.core.config import RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED +from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID +from testcontainers.core.utils import setup_logger +from testcontainers.core.waiting_utils import wait_for_logs if TYPE_CHECKING: - from .container import DockerContainer + from testcontainers.core.container import DockerContainer logger = setup_logger(__name__) From 904ae1cc49387a259bb93d48cc9f3608322a2538 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 13:43:02 +0100 Subject: [PATCH 33/38] Add env variable docs to README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 84f40b61..d982d290 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,12 @@ For more information, see [the docs][readthedocs]. ``` The snippet above will spin up a postgres database in a container. The `get_connection_url()` convenience method returns a `sqlalchemy` compatible url we use to connect to the database and retrieve the database version. + +## Configuration + +| Env Variable | Example | 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 | +| `RYUK_CONTAINER_IMAGE` | `testcontainers/ryuk:0.5.1` | Custom image for ryuk | \ No newline at end of file From 74c82aff71b2cb37be200806cae14177bb4a6db2 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Mon, 11 Mar 2024 16:18:24 +0100 Subject: [PATCH 34/38] Move Reaper class into container.py to avoid circular dependency --- core/testcontainers/core/container.py | 98 ++++++++++++++++++++++----- core/testcontainers/core/reaper.py | 68 ------------------- core/tests/test_ryuk.py | 2 +- 3 files changed, 81 insertions(+), 87 deletions(-) delete mode 100644 core/testcontainers/core/reaper.py diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 1f0f3d8a..09014caf 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -1,14 +1,18 @@ -from platform import system -from typing import Optional +from __future__ import annotations -from docker.models.containers import Container +from platform import system +from socket import socket +from typing import TYPE_CHECKING, Optional -from testcontainers.core.config import RYUK_DISABLED, RYUK_IMAGE +from testcontainers.core.config import RYUK_DISABLED, RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED from testcontainers.core.docker_client import DockerClient from testcontainers.core.exceptions import ContainerStartException -from testcontainers.core.reaper import Reaper +from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID from testcontainers.core.utils import inside_container, is_arm, setup_logger -from testcontainers.core.waiting_utils import wait_container_is_ready +from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs + +if TYPE_CHECKING: + from docker.models.containers import Container logger = setup_logger(__name__) @@ -26,7 +30,12 @@ class DockerContainer: ... delay = wait_for_logs(container, "Hello from Docker!") """ - def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs) -> None: + def __init__( + self, + image: str, + docker_client_kw: Optional[dict] = None, # noqa: UP007 Support Python3.9 type annotations + **kwargs, + ) -> None: self.env = {} self.ports = {} self.volumes = {} @@ -37,29 +46,31 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs self._name = None self._kwargs = kwargs - def with_env(self, key: str, value: str) -> "DockerContainer": + def with_env(self, key: str, value: str) -> DockerContainer: self.env[key] = value return self - def with_bind_ports(self, container: int, host: Optional[int] = None) -> "DockerContainer": + def with_bind_ports( + self, container: int, host: Optional[int] = None # noqa: UP007 Support Python3.9 type annotations + ) -> DockerContainer: self.ports[container] = host return self - def with_exposed_ports(self, *ports: int) -> "DockerContainer": + def with_exposed_ports(self, *ports: int) -> DockerContainer: for port in ports: self.ports[port] = None return self - def with_kwargs(self, **kwargs) -> "DockerContainer": + def with_kwargs(self, **kwargs) -> DockerContainer: self._kwargs = kwargs return self - def maybe_emulate_amd64(self) -> "DockerContainer": + def maybe_emulate_amd64(self) -> DockerContainer: if is_arm(): return self.with_kwargs(platform="linux/amd64") return self - def start(self) -> "DockerContainer": + def start(self) -> DockerContainer: if not RYUK_DISABLED and self.image != RYUK_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() @@ -73,7 +84,7 @@ def start(self) -> "DockerContainer": ports=self.ports, name=self._name, volumes=self.volumes, - **self._kwargs + **self._kwargs, ) logger.info("Container started: %s", self._container.short_id) return self @@ -82,7 +93,7 @@ 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) -> "DockerContainer": + def __enter__(self) -> DockerContainer: return self.start() def __exit__(self, exc_type, exc_val, exc_tb) -> None: @@ -122,15 +133,15 @@ def get_exposed_port(self, port: int) -> str: return port return mapped_port - def with_command(self, command: str) -> "DockerContainer": + def with_command(self, command: str) -> DockerContainer: self._command = command return self - def with_name(self, name: str) -> "DockerContainer": + def with_name(self, name: str) -> DockerContainer: self._name = name return self - def with_volume_mapping(self, host: str, container: str, mode: str = "ro") -> "DockerContainer": + def with_volume_mapping(self, host: str, container: str, mode: str = "ro") -> DockerContainer: mapping = {"bind": container, "mode": mode} self.volumes[host] = mapping return self @@ -150,3 +161,54 @@ def exec(self, command) -> tuple[int, str]: if not self._container: raise ContainerStartException("Container should be started before executing a command") return self._container.exec_run(command) + + +class Reaper: + _instance: Optional[Reaper] = None # noqa: UP007 Support Python3.9 type annotations + _container: Optional[DockerContainer] = None # noqa: UP007 Support Python3.9 type annotations + _socket: Optional[socket] = None # noqa: UP007 Support Python3.9 type annotations + + @classmethod + def get_instance(cls) -> Reaper: + if not Reaper._instance: + Reaper._instance = Reaper._create_instance() + + return Reaper._instance + + @classmethod + def delete_instance(cls) -> None: + if Reaper._socket is not None: + Reaper._socket.close() + Reaper._socket = None + + if Reaper._container is not None: + Reaper._container.stop() + Reaper._container = None + + if Reaper._instance is not None: + Reaper._instance = None + + @classmethod + def _create_instance(cls) -> Reaper: + logger.debug(f"Creating new Reaper for session: {SESSION_ID}") + + Reaper._container = ( + DockerContainer(RYUK_IMAGE) + .with_name(f"testcontainers-ryuk-{SESSION_ID}") + .with_exposed_ports(8080) + .with_volume_mapping(RYUK_DOCKER_SOCKET, "/var/run/docker.sock", "rw") + .with_kwargs(privileged=RYUK_PRIVILEGED) + .start() + ) + wait_for_logs(Reaper._container, r".* Started!") + + container_host = Reaper._container.get_container_host_ip() + container_port = int(Reaper._container.get_exposed_port(8080)) + + Reaper._socket = socket() + Reaper._socket.connect((container_host, container_port)) + Reaper._socket.send(f"label={LABEL_SESSION_ID}={SESSION_ID}\r\n".encode()) + + Reaper._instance = Reaper() + + return Reaper._instance diff --git a/core/testcontainers/core/reaper.py b/core/testcontainers/core/reaper.py deleted file mode 100644 index 82123d7f..00000000 --- a/core/testcontainers/core/reaper.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -from socket import socket -from typing import TYPE_CHECKING, Optional - -from testcontainers.core.config import RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED -from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID -from testcontainers.core.utils import setup_logger -from testcontainers.core.waiting_utils import wait_for_logs - -if TYPE_CHECKING: - from testcontainers.core.container import DockerContainer - - -logger = setup_logger(__name__) - - -class Reaper: - _instance: Optional[Reaper] = None # noqa: UP007 Support Python3.9 type annotations - _container: "Optional[DockerContainer]" = None # noqa: UP037,UP007 Quotes for type annotation due to circular deps - _socket: Optional[socket] = None # noqa: UP007 Support Python3.9 type annotations - - @classmethod - def get_instance(cls) -> Reaper: - if not Reaper._instance: - Reaper._instance = Reaper._create_instance() - - return Reaper._instance - - @classmethod - def delete_instance(cls) -> None: - if Reaper._socket is not None: - Reaper._socket.close() - Reaper._socket = None - - if Reaper._container is not None: - Reaper._container.stop() - Reaper._container = None - - if Reaper._instance is not None: - Reaper._instance = None - - @classmethod - def _create_instance(cls) -> Reaper: - from .container import DockerContainer - - logger.debug(f"Creating new Reaper for session: {SESSION_ID}") - - Reaper._container = ( - DockerContainer(RYUK_IMAGE) - .with_name(f"testcontainers-ryuk-{SESSION_ID}") - .with_exposed_ports(8080) - .with_volume_mapping(RYUK_DOCKER_SOCKET, "/var/run/docker.sock", "rw") - .with_kwargs(privileged=RYUK_PRIVILEGED) - .start() - ) - wait_for_logs(Reaper._container, r".* Started!") - - container_host = Reaper._container.get_container_host_ip() - container_port = int(Reaper._container.get_exposed_port(8080)) - - Reaper._socket = socket() - Reaper._socket.connect((container_host, container_port)) - Reaper._socket.send(f"label={LABEL_SESSION_ID}={SESSION_ID}\r\n".encode()) - - Reaper._instance = Reaper() - - return Reaper._instance diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index 48373fb8..32370ffb 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -1,5 +1,5 @@ from testcontainers.core import container -from testcontainers.core.reaper import Reaper +from testcontainers.core.container import Reaper from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs From ed47f5718ff7101a09ba56ec09cc0b4f147531d4 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 12 Mar 2024 09:12:13 +0100 Subject: [PATCH 35/38] Add missing newline to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d982d290..7f469914 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,4 @@ The snippet above will spin up a postgres database in a container. The `get_conn | `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 | -| `RYUK_CONTAINER_IMAGE` | `testcontainers/ryuk:0.5.1` | Custom image for ryuk | \ No newline at end of file +| `RYUK_CONTAINER_IMAGE` | `testcontainers/ryuk:0.5.1` | Custom image for ryuk | From 5772dca4bdef34474c16ca76eb56115d212bf8db Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Tue, 12 Mar 2024 09:29:50 +0100 Subject: [PATCH 36/38] Remove noqa lint rule exceptions The import from __future__ import annotations effectively adds support for native union syntax (|) because type hints are treated as strings at runtime. --- core/testcontainers/core/container.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 09014caf..01638df2 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -2,7 +2,7 @@ from platform import system from socket import socket -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from testcontainers.core.config import RYUK_DISABLED, RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED from testcontainers.core.docker_client import DockerClient @@ -33,7 +33,7 @@ class DockerContainer: def __init__( self, image: str, - docker_client_kw: Optional[dict] = None, # noqa: UP007 Support Python3.9 type annotations + docker_client_kw: dict | None = None, **kwargs, ) -> None: self.env = {} @@ -50,9 +50,7 @@ def with_env(self, key: str, value: str) -> DockerContainer: self.env[key] = value return self - def with_bind_ports( - self, container: int, host: Optional[int] = None # noqa: UP007 Support Python3.9 type annotations - ) -> DockerContainer: + def with_bind_ports(self, container: int, host: int | None = None) -> DockerContainer: self.ports[container] = host return self @@ -164,9 +162,9 @@ def exec(self, command) -> tuple[int, str]: class Reaper: - _instance: Optional[Reaper] = None # noqa: UP007 Support Python3.9 type annotations - _container: Optional[DockerContainer] = None # noqa: UP007 Support Python3.9 type annotations - _socket: Optional[socket] = None # noqa: UP007 Support Python3.9 type annotations + _instance: Reaper | None = None + _container: DockerContainer | None = None + _socket: socket | None = None @classmethod def get_instance(cls) -> Reaper: From da6f3d80bd3e31e313c30746594abe43e1912362 Mon Sep 17 00:00:00 2001 From: Balint Bartha <39852431+totallyzen@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:12:51 +0100 Subject: [PATCH 37/38] fix: linting with ruff, keep runtime typging --- core/testcontainers/core/container.py | 12 ++++++------ pyproject.toml | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 01638df2..60291227 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -2,7 +2,7 @@ from platform import system from socket import socket -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from testcontainers.core.config import RYUK_DISABLED, RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED from testcontainers.core.docker_client import DockerClient @@ -33,7 +33,7 @@ class DockerContainer: def __init__( self, image: str, - docker_client_kw: dict | None = None, + docker_client_kw: Optional[dict] = None, **kwargs, ) -> None: self.env = {} @@ -50,7 +50,7 @@ def with_env(self, key: str, value: str) -> DockerContainer: self.env[key] = value return self - def with_bind_ports(self, container: int, host: int | None = None) -> DockerContainer: + def with_bind_ports(self, container: int, host: Optional[int] = None) -> DockerContainer: self.ports[container] = host return self @@ -162,9 +162,9 @@ def exec(self, command) -> tuple[int, str]: class Reaper: - _instance: Reaper | None = None - _container: DockerContainer | None = None - _socket: socket | None = None + _instance: Optional[Reaper] = None + _container: Optional[DockerContainer] = None + _socket: Optional[socket] = None @classmethod def get_instance(cls) -> Reaper: diff --git a/pyproject.toml b/pyproject.toml index e1008e8d..061cf6c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,6 +197,9 @@ ignore = [ "INP001" ] +[tool.ruff.lint.pyupgrade] +keep-runtime-typing = true + [tool.ruff.lint.flake8-type-checking] strict = true From 81cb09e15703b1b78686ddb4581358f0081f8949 Mon Sep 17 00:00:00 2001 From: Vemund Santi Date: Thu, 14 Mar 2024 10:30:49 +0100 Subject: [PATCH 38/38] Replace future import with quoted type hints --- core/testcontainers/core/container.py | 30 +++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 60291227..f0da90bb 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from platform import system from socket import socket from typing import TYPE_CHECKING, Optional @@ -46,29 +44,29 @@ def __init__( self._name = None self._kwargs = kwargs - def with_env(self, key: str, value: str) -> DockerContainer: + def with_env(self, key: str, value: str) -> "DockerContainer": self.env[key] = value return self - def with_bind_ports(self, container: int, host: Optional[int] = None) -> DockerContainer: + def with_bind_ports(self, container: int, host: Optional[int] = None) -> "DockerContainer": self.ports[container] = host return self - def with_exposed_ports(self, *ports: int) -> DockerContainer: + def with_exposed_ports(self, *ports: int) -> "DockerContainer": for port in ports: self.ports[port] = None return self - def with_kwargs(self, **kwargs) -> DockerContainer: + def with_kwargs(self, **kwargs) -> "DockerContainer": self._kwargs = kwargs return self - def maybe_emulate_amd64(self) -> DockerContainer: + def maybe_emulate_amd64(self) -> "DockerContainer": if is_arm(): return self.with_kwargs(platform="linux/amd64") return self - def start(self) -> DockerContainer: + def start(self): if not RYUK_DISABLED and self.image != RYUK_IMAGE: logger.debug("Creating Ryuk container") Reaper.get_instance() @@ -91,7 +89,7 @@ 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) -> DockerContainer: + def __enter__(self): return self.start() def __exit__(self, exc_type, exc_val, exc_tb) -> None: @@ -131,20 +129,20 @@ def get_exposed_port(self, port: int) -> str: return port return mapped_port - def with_command(self, command: str) -> DockerContainer: + def with_command(self, command: str) -> "DockerContainer": self._command = command return self - def with_name(self, name: str) -> DockerContainer: + def with_name(self, name: str) -> "DockerContainer": self._name = name return self - def with_volume_mapping(self, host: str, container: str, mode: str = "ro") -> DockerContainer: + def with_volume_mapping(self, host: str, container: str, mode: str = "ro") -> "DockerContainer": mapping = {"bind": container, "mode": mode} self.volumes[host] = mapping return self - def get_wrapped_container(self) -> Container: + def get_wrapped_container(self) -> "Container": return self._container def get_docker_client(self) -> DockerClient: @@ -162,12 +160,12 @@ def exec(self, command) -> tuple[int, str]: class Reaper: - _instance: Optional[Reaper] = None + _instance: "Optional[Reaper]" = None _container: Optional[DockerContainer] = None _socket: Optional[socket] = None @classmethod - def get_instance(cls) -> Reaper: + def get_instance(cls) -> "Reaper": if not Reaper._instance: Reaper._instance = Reaper._create_instance() @@ -187,7 +185,7 @@ def delete_instance(cls) -> None: Reaper._instance = None @classmethod - def _create_instance(cls) -> Reaper: + def _create_instance(cls) -> "Reaper": logger.debug(f"Creating new Reaper for session: {SESSION_ID}") Reaper._container = (