Skip to content
Closed
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
2 changes: 1 addition & 1 deletion core/testcontainers/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
15 changes: 14 additions & 1 deletion core/testcontainers/core/container.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
from platform import system
from socket import socket
from typing import TYPE_CHECKING, Optional
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 19 additions & 0 deletions core/testcontainers/core/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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)
6 changes: 4 additions & 2 deletions core/tests/test_docker_in_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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():
Expand All @@ -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()
Expand All @@ -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()
4 changes: 4 additions & 0 deletions core/tests/test_ryuk.py
Original file line number Diff line number Diff line change
@@ -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!")
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
+-------------------------------------------+-------------------------------+------------------------------------------+
Expand Down