Skip to content

fix(ryuk): Enable Ryuk test suite. Ryuk image 0.5.1 -> 0.7.0. Add RYUK_RECONNECTION_TIMEOUT env variable #509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 30, 2024
Merged
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
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ The snippet above will spin up a postgres database in a container. The `get_conn

## 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 |
| 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.7.0` | Custom image for ryuk |
| `RYUK_RECONNECTION_TIMEOUT` | `10s` | Reconnection timeout for Ryuk TCP socket before Ryuk reaps all dangling containers |
3 changes: 2 additions & 1 deletion core/testcontainers/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
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.5.1")
RYUK_IMAGE: str = environ.get("RYUK_CONTAINER_IMAGE", "testcontainers/ryuk:0.7.0")
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_RECONNECTION_TIMEOUT: str = environ.get("RYUK_RECONNECTION_TIMEOUT", "10s")
17 changes: 14 additions & 3 deletions core/testcontainers/core/container.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import contextlib
from platform import system
from socket import socket
from typing import TYPE_CHECKING, Optional

from testcontainers.core.config import RYUK_DISABLED, RYUK_DOCKER_SOCKET, RYUK_IMAGE, RYUK_PRIVILEGED
import docker.errors

from testcontainers.core.config import (
RYUK_DISABLED,
RYUK_DOCKER_SOCKET,
RYUK_IMAGE,
RYUK_PRIVILEGED,
RYUK_RECONNECTION_TIMEOUT,
)
from testcontainers.core.docker_client import DockerClient
from testcontainers.core.exceptions import ContainerStartException
from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID
Expand Down Expand Up @@ -177,8 +186,9 @@ def delete_instance(cls) -> None:
Reaper._socket.close()
Reaper._socket = None

if Reaper._container is not None:
Reaper._container.stop()
if Reaper._container is not None and Reaper._container._container is not None:
with contextlib.suppress(docker.errors.NotFound):
Reaper._container.stop()
Reaper._container = None

if Reaper._instance is not None:
Expand All @@ -194,6 +204,7 @@ def _create_instance(cls) -> "Reaper":
.with_exposed_ports(8080)
.with_volume_mapping(RYUK_DOCKER_SOCKET, "/var/run/docker.sock", "rw")
.with_kwargs(privileged=RYUK_PRIVILEGED, auto_remove=True)
.with_env("RYUK_RECONNECTION_TIMEOUT", RYUK_RECONNECTION_TIMEOUT)
.start()
)
wait_for_logs(Reaper._container, r".* Started!")
Expand Down
59 changes: 40 additions & 19 deletions core/tests/test_ryuk.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
from contextlib import contextmanager

from time import sleep
import pytest
from pytest import MonkeyPatch

from docker import DockerClient
from docker.errors import NotFound

from testcontainers.core import container
from testcontainers.core import container as container_module
from testcontainers.core.container import Reaper
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs


@pytest.mark.skip("invalid test - ryuk logs 'Removed' right before exiting")
def test_wait_for_reaper():
def test_wait_for_reaper(monkeypatch: MonkeyPatch):
Reaper.delete_instance()
monkeypatch.setattr(container_module, "RYUK_RECONNECTION_TIMEOUT", "0.1s")
docker_client = DockerClient()
container = DockerContainer("hello-world").start()

container_id = container.get_wrapped_container().short_id
reaper_id = Reaper._container.get_wrapped_container().short_id

assert docker_client.containers.get(container_id) is not None
assert docker_client.containers.get(reaper_id) is not None

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 \d .*", timeout=30)
sleep(0.6) # Sleep until Ryuk reaps all dangling containers. 0.5 extra seconds for good measure.

with pytest.raises(NotFound):
docker_client.containers.get(container_id)
with pytest.raises(NotFound):
docker_client.containers.get(reaper_id)

# Cleanup Ryuk class fields after manual Ryuk shutdown
Reaper.delete_instance()


def test_container_without_ryuk(monkeypatch: MonkeyPatch):
Reaper.delete_instance()
monkeypatch.setattr(container_module, "RYUK_DISABLED", True)
with DockerContainer("hello-world") as container:
wait_for_logs(container, "Hello from Docker!")
assert Reaper._instance is None


@contextmanager
def reset_reaper_instance():
old_value = Reaper._instance
Reaper._instance = None
yield
Reaper._instance = old_value
def test_ryuk_is_reused_in_same_process():
with DockerContainer("hello-world") as container:
wait_for_logs(container, "Hello from Docker!")
reaper_instance = Reaper._instance

assert reaper_instance is not None

def test_container_without_ryuk(monkeypatch):
monkeypatch.setattr(container, "RYUK_DISABLED", True)
with reset_reaper_instance(), DockerContainer("hello-world") as cont:
wait_for_logs(cont, "Hello from Docker!")
assert Reaper._instance is None
with DockerContainer("hello-world") as container:
wait_for_logs(container, "Hello from Docker!")
assert reaper_instance is Reaper._instance
2 changes: 1 addition & 1 deletion index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Configuration
+-------------------------------------------+-------------------------------+------------------------------------------+
| ``TESTCONTAINERS_RYUK_DISABLED`` | ``false`` | Disable ryuk |
+-------------------------------------------+-------------------------------+------------------------------------------+
| ``RYUK_CONTAINER_IMAGE`` | ``testcontainers/ryuk:0.5.1`` | Custom image for ryuk |
| ``RYUK_CONTAINER_IMAGE`` | ``testcontainers/ryuk:0.7.0`` | Custom image for ryuk |
+-------------------------------------------+-------------------------------+------------------------------------------+

Development and Contributing
Expand Down