From 9c3ecc85b436cc2ad308c7c9617427b927a88697 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Wed, 5 Jul 2023 14:35:30 +0200 Subject: [PATCH 01/15] Add network context manager --- core/testcontainers/core/network.py | 44 +++++++++++++++++++++++++++++ core/tests/test_network.py | 27 ++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 core/testcontainers/core/network.py create mode 100644 core/tests/test_network.py diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py new file mode 100644 index 000000000..7449ed496 --- /dev/null +++ b/core/testcontainers/core/network.py @@ -0,0 +1,44 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from typing import Optional + +from testcontainers.core.docker_client import DockerClient + + +class Network(object): + """ + Network context manager to conveniently connect containers. + """ + + def __init__(self, name, docker_client_kw: Optional[dict] = None, **kwargs) -> None: + self.name = name + self._docker = DockerClient(**(docker_client_kw or {})) + self._kwargs = kwargs + + def remove(self) -> None: + self._network.remove() + + def __enter__(self) -> 'Network': + self._network = self._docker.client.networks.create(self.name, **self._kwargs) + self.id = self._network.id + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.remove() + + def __del__(self) -> None: + if self._network is not None: + try: + self.remove() + except: # noqa: E722 + pass diff --git a/core/tests/test_network.py b/core/tests/test_network.py new file mode 100644 index 000000000..488941a7d --- /dev/null +++ b/core/tests/test_network.py @@ -0,0 +1,27 @@ +from testcontainers.core.container import DockerContainer +from testcontainers.core.docker_client import DockerClient +from testcontainers.core.network import Network + + +def test_network_gets_created_and_cleaned_up(): + with Network("test-network") as network: + docker = DockerClient() + networks_list = docker.client.networks.list("test-network") + assert networks_list[0].name == "test-network" + assert networks_list[0].id == network.id + assert not docker.client.networks.list("test-network") + + +def test_containers_can_communicate_over_network(): + with Network("network") as network: + with DockerContainer("nginx:alpine-slim").with_name( + "alpine1").with_kwargs(network=network.name) as alpine1: + with DockerContainer("nginx:alpine-slim").with_name( + "alpine2").with_kwargs(network=network.name) as alpine2: + status, output = alpine1.exec("ping -c 1 alpine2") + assert status == 0 + assert "64 bytes" in str(output) + + status, output = alpine2.exec("ping -c 1 alpine1") + assert status == 0 + assert "64 bytes" in str(output) From c10223fbec1d88755816c0ba9d2f77693816d822 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Mon, 27 Nov 2023 11:32:41 +0100 Subject: [PATCH 02/15] Use random names for networks --- core/testcontainers/core/network.py | 7 ++++--- core/tests/test_network.py | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index 7449ed496..f152c6e83 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -10,6 +10,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import uuid from typing import Optional from testcontainers.core.docker_client import DockerClient @@ -17,11 +18,11 @@ class Network(object): """ - Network context manager to conveniently connect containers. + Network context manager for programmatically connecting containers. """ - def __init__(self, name, docker_client_kw: Optional[dict] = None, **kwargs) -> None: - self.name = name + def __init__(self, docker_client_kw: Optional[dict] = None, **kwargs) -> None: + self.name = str(uuid.uuid4()) self._docker = DockerClient(**(docker_client_kw or {})) self._kwargs = kwargs diff --git a/core/tests/test_network.py b/core/tests/test_network.py index 488941a7d..c5de10bc4 100644 --- a/core/tests/test_network.py +++ b/core/tests/test_network.py @@ -4,16 +4,16 @@ def test_network_gets_created_and_cleaned_up(): - with Network("test-network") as network: + with Network() as network: docker = DockerClient() - networks_list = docker.client.networks.list("test-network") - assert networks_list[0].name == "test-network" + networks_list = docker.client.networks.list(network.name) + assert networks_list[0].name == network.name assert networks_list[0].id == network.id - assert not docker.client.networks.list("test-network") + assert not docker.client.networks.list(network.name) def test_containers_can_communicate_over_network(): - with Network("network") as network: + with Network() as network: with DockerContainer("nginx:alpine-slim").with_name( "alpine1").with_kwargs(network=network.name) as alpine1: with DockerContainer("nginx:alpine-slim").with_name( From e4d9c19b0a3f6685d9bb1ef880420882bee6b971 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Tue, 28 Nov 2023 17:01:09 +0100 Subject: [PATCH 03/15] Add explicit API DockerContainer.with_network(Network) --- core/testcontainers/core/container.py | 8 ++++++++ core/testcontainers/core/network.py | 3 +++ core/tests/test_network.py | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 65acf9bed..4b1b1b897 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -2,6 +2,7 @@ import os from typing import Iterable, Optional, Tuple +from .network import Network from .waiting_utils import wait_container_is_ready from .docker_client import DockerClient from .exceptions import ContainerStartException @@ -31,6 +32,7 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs self._container = None self._command = None self._name = None + self._network: Optional[Network] = None self._kwargs = kwargs def with_env(self, key: str, value: str) -> 'DockerContainer': @@ -46,6 +48,10 @@ def with_exposed_ports(self, *ports: Iterable[int]) -> 'DockerContainer': self.ports[port] = None return self + def with_network(self, network: Network) -> 'DockerContainer': + self._network = network + return self + def with_kwargs(self, **kwargs) -> 'DockerContainer': self._kwargs = kwargs return self @@ -63,6 +69,8 @@ def start(self) -> 'DockerContainer': name=self._name, volumes=self.volumes, **self._kwargs ) logger.info("Container started: %s", self._container.short_id) + if self._network: + self._network.connect(self._container.id) return self def stop(self, force=True, delete_volume=True) -> None: diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index f152c6e83..936d223b4 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -26,6 +26,9 @@ def __init__(self, docker_client_kw: Optional[dict] = None, **kwargs) -> None: self._docker = DockerClient(**(docker_client_kw or {})) self._kwargs = kwargs + def connect(self, container_id: str): + self._network.connect(container_id) + def remove(self) -> None: self._network.remove() diff --git a/core/tests/test_network.py b/core/tests/test_network.py index c5de10bc4..9e7dda0e1 100644 --- a/core/tests/test_network.py +++ b/core/tests/test_network.py @@ -15,9 +15,9 @@ def test_network_gets_created_and_cleaned_up(): def test_containers_can_communicate_over_network(): with Network() as network: with DockerContainer("nginx:alpine-slim").with_name( - "alpine1").with_kwargs(network=network.name) as alpine1: + "alpine1").with_network(network) as alpine1: with DockerContainer("nginx:alpine-slim").with_name( - "alpine2").with_kwargs(network=network.name) as alpine2: + "alpine2").with_network(network) as alpine2: status, output = alpine1.exec("ping -c 1 alpine2") assert status == 0 assert "64 bytes" in str(output) From c7571426c270c3785a352ef22957591b2e015eee Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Tue, 28 Nov 2023 18:03:09 +0100 Subject: [PATCH 04/15] Add support for network aliases --- core/testcontainers/core/container.py | 7 +++++- core/testcontainers/core/network.py | 4 +-- core/tests/test_network.py | 35 ++++++++++++++++++--------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 4b1b1b897..2b71cc0e8 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -33,6 +33,7 @@ def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs self._command = None self._name = None self._network: Optional[Network] = None + self._network_aliases: Optional[list] = None self._kwargs = kwargs def with_env(self, key: str, value: str) -> 'DockerContainer': @@ -52,6 +53,10 @@ def with_network(self, network: Network) -> 'DockerContainer': self._network = network return self + def with_network_aliases(self, *aliases) -> 'DockerContainer': + self._network_aliases = aliases + return self + def with_kwargs(self, **kwargs) -> 'DockerContainer': self._kwargs = kwargs return self @@ -70,7 +75,7 @@ def start(self) -> 'DockerContainer': ) logger.info("Container started: %s", self._container.short_id) if self._network: - self._network.connect(self._container.id) + self._network.connect(self._container.id, self._network_aliases) return self def stop(self, force=True, delete_volume=True) -> None: diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index 936d223b4..8444b3f62 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -26,8 +26,8 @@ def __init__(self, docker_client_kw: Optional[dict] = None, **kwargs) -> None: self._docker = DockerClient(**(docker_client_kw or {})) self._kwargs = kwargs - def connect(self, container_id: str): - self._network.connect(container_id) + def connect(self, container_id: str, network_aliases: Optional[list] = None): + self._network.connect(container_id, aliases=network_aliases) def remove(self) -> None: self._network.remove() diff --git a/core/tests/test_network.py b/core/tests/test_network.py index 9e7dda0e1..2e490a4e6 100644 --- a/core/tests/test_network.py +++ b/core/tests/test_network.py @@ -2,6 +2,8 @@ from testcontainers.core.docker_client import DockerClient from testcontainers.core.network import Network +NGINX_ALPINE_SLIM_IMAGE = "nginx:alpine-slim" + def test_network_gets_created_and_cleaned_up(): with Network() as network: @@ -14,14 +16,25 @@ def test_network_gets_created_and_cleaned_up(): def test_containers_can_communicate_over_network(): with Network() as network: - with DockerContainer("nginx:alpine-slim").with_name( - "alpine1").with_network(network) as alpine1: - with DockerContainer("nginx:alpine-slim").with_name( - "alpine2").with_network(network) as alpine2: - status, output = alpine1.exec("ping -c 1 alpine2") - assert status == 0 - assert "64 bytes" in str(output) - - status, output = alpine2.exec("ping -c 1 alpine1") - assert status == 0 - assert "64 bytes" in str(output) + with DockerContainer(NGINX_ALPINE_SLIM_IMAGE)\ + .with_name("alpine1")\ + .with_network_aliases("alpine1-alias-1", "alpine1-alias-2")\ + .with_network(network) as alpine1: + with DockerContainer(NGINX_ALPINE_SLIM_IMAGE)\ + .with_name("alpine2")\ + .with_network_aliases("alpine2-alias-1", "alpine2-alias-2")\ + .with_network(network) as alpine2: + + assert_can_ping(alpine1, "alpine2") + assert_can_ping(alpine1, "alpine2-alias-1") + assert_can_ping(alpine1, "alpine2-alias-2") + + assert_can_ping(alpine2, "alpine1") + assert_can_ping(alpine2, "alpine1-alias-1") + assert_can_ping(alpine2, "alpine1-alias-2") + + +def assert_can_ping(container: DockerContainer, remote_name: str): + status, output = container.exec("ping -c 1 %s" % remote_name) + assert status == 0 + assert "64 bytes" in str(output) From 1c8ebdc26a42a10f2319fe127819006bd8a1f0cd Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Thu, 4 Apr 2024 17:40:18 +0200 Subject: [PATCH 05/15] chore: change return type to Self --- 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 9e0d117a3..495b479a9 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -70,11 +70,11 @@ def with_exposed_ports(self, *ports: int) -> Self: self.ports[port] = None return self - def with_network(self, network: Network) -> 'DockerContainer': + def with_network(self, network: Network) -> Self: self._network = network return self - def with_network_aliases(self, *aliases) -> 'DockerContainer': + def with_network_aliases(self, *aliases) -> Self: self._network_aliases = aliases return self From cac19d39176c1758ecb4878614ace3bb0d3cb6a8 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Mon, 8 Apr 2024 11:18:20 +0200 Subject: [PATCH 06/15] chore: further specify type --- 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 495b479a9..639980440 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -54,7 +54,7 @@ def __init__( self._command = None self._name = None self._network: Optional[Network] = None - self._network_aliases: Optional[list] = None + self._network_aliases: Optional[list[str]] = None self._kwargs = kwargs def with_env(self, key: str, value: str) -> Self: From b392ca64f336ac0cf74f81180dc55f4c24819107 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Mon, 8 Apr 2024 11:23:04 +0200 Subject: [PATCH 07/15] chore: clarify Network init signature --- core/testcontainers/core/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index 8444b3f62..fbed4215f 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -21,10 +21,10 @@ class Network(object): Network context manager for programmatically connecting containers. """ - def __init__(self, docker_client_kw: Optional[dict] = None, **kwargs) -> None: + def __init__(self, docker_client_kw: Optional[dict] = None, docker_network_kw: Optional[dict] = None) -> None: self.name = str(uuid.uuid4()) self._docker = DockerClient(**(docker_client_kw or {})) - self._kwargs = kwargs + self._docker_network_kw = docker_network_kw or {} def connect(self, container_id: str, network_aliases: Optional[list] = None): self._network.connect(container_id, aliases=network_aliases) @@ -33,7 +33,7 @@ def remove(self) -> None: self._network.remove() def __enter__(self) -> 'Network': - self._network = self._docker.client.networks.create(self.name, **self._kwargs) + self._network = self._docker.client.networks.create(self.name, **self._docker_network_kw) self.id = self._network.id return self From 8c8b6e2945fea7dbdca1fc6dfa68f2faaf4d956a Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Mon, 8 Apr 2024 11:29:19 +0200 Subject: [PATCH 08/15] chore: remove __del__hook --- core/testcontainers/core/network.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index fbed4215f..9ff1e843b 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -39,10 +39,3 @@ def __enter__(self) -> 'Network': def __exit__(self, exc_type, exc_val, exc_tb) -> None: self.remove() - - def __del__(self) -> None: - if self._network is not None: - try: - self.remove() - except: # noqa: E722 - pass From a64625cc62ed8d4e364a4b8c4116d5690885f548 Mon Sep 17 00:00:00 2001 From: Max Pfeiffer Date: Fri, 5 Apr 2024 12:33:09 +0200 Subject: [PATCH 09/15] chore(codestyle): switch to `ruff` from `black` for code formatting (#529) Changed pre-commit config. Some files became re-formatted. --- .pre-commit-config.yaml | 10 +++------- core/testcontainers/core/docker_client.py | 1 - modules/influxdb/testcontainers/influxdb.py | 1 + modules/mongodb/testcontainers/mongodb/__init__.py | 2 +- modules/mssql/testcontainers/mssql/__init__.py | 2 +- modules/mysql/testcontainers/mysql/__init__.py | 2 +- modules/mysql/tests/test_mysql.py | 7 ++++--- .../opensearch/testcontainers/opensearch/__init__.py | 2 +- modules/oracle-free/testcontainers/oracle/__init__.py | 2 +- modules/rabbitmq/testcontainers/rabbitmq/__init__.py | 2 +- 10 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5b94bdde..5808a0000 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,18 +9,14 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - - repo: https://github.com/psf/black-pre-commit-mirror - rev: '24.2.0' - hooks: - - id: black - args: [ '--config', 'pyproject.toml' ] - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.3.0' + rev: 'v0.3.5' hooks: - id: ruff # Explicitly setting config to prevent Ruff from using `pyproject.toml` in sub packages. args: [ '--fix', '--exit-non-zero-on-fix', '--config', 'pyproject.toml' ] + - id: ruff-format + args: [ '--config', 'pyproject.toml' ] # - repo: local # hooks: diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 22a9a4ef8..89db0fbfc 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -36,7 +36,6 @@ def _wrapped_container_collection(function: Callable[_P, _T]) -> Callable[_P, _T]: - @ft.wraps(ContainerCollection.run) def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T: return function(*args, **kwargs) diff --git a/modules/influxdb/testcontainers/influxdb.py b/modules/influxdb/testcontainers/influxdb.py index 4b9d9b905..d8956e992 100644 --- a/modules/influxdb/testcontainers/influxdb.py +++ b/modules/influxdb/testcontainers/influxdb.py @@ -26,6 +26,7 @@ - because the InfluxDB clients are different for 1.x and 2.x versions, so you won't have to install dependencies that you do not need """ + from typing import Optional from requests import get diff --git a/modules/mongodb/testcontainers/mongodb/__init__.py b/modules/mongodb/testcontainers/mongodb/__init__.py index 32e1f7484..4a436b195 100644 --- a/modules/mongodb/testcontainers/mongodb/__init__.py +++ b/modules/mongodb/testcontainers/mongodb/__init__.py @@ -53,7 +53,7 @@ def __init__( username: Optional[str] = None, password: Optional[str] = None, dbname: Optional[str] = None, - **kwargs + **kwargs, ) -> None: raise_for_deprecated_parameter(kwargs, "port_to_expose", "port") super().__init__(image=image, **kwargs) diff --git a/modules/mssql/testcontainers/mssql/__init__.py b/modules/mssql/testcontainers/mssql/__init__.py index 98b668269..3bfe861b4 100644 --- a/modules/mssql/testcontainers/mssql/__init__.py +++ b/modules/mssql/testcontainers/mssql/__init__.py @@ -30,7 +30,7 @@ def __init__( port: int = 1433, dbname: str = "tempdb", dialect: str = "mssql+pymssql", - **kwargs + **kwargs, ) -> None: raise_for_deprecated_parameter(kwargs, "user", "username") super().__init__(image, **kwargs) diff --git a/modules/mysql/testcontainers/mysql/__init__.py b/modules/mysql/testcontainers/mysql/__init__.py index 65b317b0c..a5b839273 100644 --- a/modules/mysql/testcontainers/mysql/__init__.py +++ b/modules/mysql/testcontainers/mysql/__init__.py @@ -48,7 +48,7 @@ def __init__( password: Optional[str] = None, dbname: Optional[str] = None, port: int = 3306, - **kwargs + **kwargs, ) -> None: raise_for_deprecated_parameter(kwargs, "MYSQL_USER", "username") raise_for_deprecated_parameter(kwargs, "MYSQL_ROOT_PASSWORD", "root_password") diff --git a/modules/mysql/tests/test_mysql.py b/modules/mysql/tests/test_mysql.py index a84df4d13..3506960bf 100644 --- a/modules/mysql/tests/test_mysql.py +++ b/modules/mysql/tests/test_mysql.py @@ -40,9 +40,10 @@ def test_docker_run_mariadb(): def test_docker_env_variables(): - with mock.patch.dict("os.environ", MYSQL_USER="demo", MYSQL_DATABASE="custom_db"), MySqlContainer( - "mariadb:10.6.5" - ).with_bind_ports(3306, 32785).maybe_emulate_amd64() as container: + with ( + mock.patch.dict("os.environ", MYSQL_USER="demo", MYSQL_DATABASE="custom_db"), + MySqlContainer("mariadb:10.6.5").with_bind_ports(3306, 32785).maybe_emulate_amd64() as container, + ): url = container.get_connection_url() pattern = r"mysql\+pymysql:\/\/demo:test@[\w,.]+:(3306|32785)\/custom_db" assert re.match(pattern, url) diff --git a/modules/opensearch/testcontainers/opensearch/__init__.py b/modules/opensearch/testcontainers/opensearch/__init__.py index f889c9934..06d3a7671 100644 --- a/modules/opensearch/testcontainers/opensearch/__init__.py +++ b/modules/opensearch/testcontainers/opensearch/__init__.py @@ -35,7 +35,7 @@ def __init__( image: str = "opensearchproject/opensearch:2.4.0", port: int = 9200, security_enabled: bool = False, - **kwargs + **kwargs, ) -> None: """ Args: diff --git a/modules/oracle-free/testcontainers/oracle/__init__.py b/modules/oracle-free/testcontainers/oracle/__init__.py index 2b903ac54..03f525a71 100644 --- a/modules/oracle-free/testcontainers/oracle/__init__.py +++ b/modules/oracle-free/testcontainers/oracle/__init__.py @@ -36,7 +36,7 @@ def __init__( password: Optional[str] = None, port: int = 1521, dbname: Optional[str] = None, - **kwargs + **kwargs, ) -> None: super().__init__(image=image, **kwargs) diff --git a/modules/rabbitmq/testcontainers/rabbitmq/__init__.py b/modules/rabbitmq/testcontainers/rabbitmq/__init__.py index 0a5486025..3e5ad0b33 100644 --- a/modules/rabbitmq/testcontainers/rabbitmq/__init__.py +++ b/modules/rabbitmq/testcontainers/rabbitmq/__init__.py @@ -31,7 +31,7 @@ def __init__( port: Optional[int] = None, username: Optional[str] = None, password: Optional[str] = None, - **kwargs + **kwargs, ) -> None: """Initialize the RabbitMQ test container. From a00d8dd981d9493e2ad872fb04de2c2200f8523d Mon Sep 17 00:00:00 2001 From: Jakob Beckmann <32326425+f4z3r@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:45:41 +0200 Subject: [PATCH 10/15] fix(vault): add support for HashiCorp Vault container (#366) Add support for a Vault container. --- index.rst | 1 + modules/vault/README.rst | 2 + .../vault/testcontainers/vault/__init__.py | 74 +++++++++++++++++++ modules/vault/tests/test_vault.py | 41 ++++++++++ poetry.lock | 20 ++++- pyproject.toml | 4 + 6 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 modules/vault/README.rst create mode 100644 modules/vault/testcontainers/vault/__init__.py create mode 100644 modules/vault/tests/test_vault.py diff --git a/index.rst b/index.rst index 2a2bc6599..4f3dad802 100644 --- a/index.rst +++ b/index.rst @@ -42,6 +42,7 @@ testcontainers-python facilitates the use of Docker containers for functional an modules/redis/README modules/registry/README modules/selenium/README + modules/vault/README modules/weaviate/README Getting Started diff --git a/modules/vault/README.rst b/modules/vault/README.rst new file mode 100644 index 000000000..71c079dfa --- /dev/null +++ b/modules/vault/README.rst @@ -0,0 +1,2 @@ +.. autoclass:: testcontainers.vault.VaultContainer +.. title:: testcontainers.vault.VaultContainer diff --git a/modules/vault/testcontainers/vault/__init__.py b/modules/vault/testcontainers/vault/__init__.py new file mode 100644 index 000000000..5f50cdd4e --- /dev/null +++ b/modules/vault/testcontainers/vault/__init__.py @@ -0,0 +1,74 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from http.client import HTTPException +from urllib.request import urlopen + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_container_is_ready + + +class VaultContainer(DockerContainer): + """ + Vault container. + + Example: + + .. doctest:: + + >>> from testcontainers.vault import VaultContainer + >>> import hvac + + >>> with VaultContainer("hashicorp/vault:1.16.1") as vault_container: + ... connection_url = vault_container.get_connection_url() + ... client = hvac.Client(url=connection_url, token=vault_container.root_token) + ... assert client.is_authenticated() + ... # use root client to perform desired actions, e.g. + ... policies = client.sys.list_acl_policies() + """ + + def __init__( + self, + image: str = "hashicorp/vault:latest", + port: int = 8200, + root_token: str = "toor", + **kwargs, + ) -> None: + super().__init__(image, **kwargs) + self.port = port + self.root_token = root_token + self.with_exposed_ports(self.port) + self.with_env("VAULT_DEV_ROOT_TOKEN_ID", self.root_token) + + def get_connection_url(self) -> str: + """ + Get the connection URL used to connect to the Vault container. + + Returns: + str: The address to connect to. + """ + host_ip = self.get_container_host_ip() + exposed_port = self.get_exposed_port(self.port) + return f"http://{host_ip}:{exposed_port}" + + @wait_container_is_ready(HTTPException) + def _healthcheck(self) -> None: + url = f"{self.get_connection_url()}/v1/sys/health" + with urlopen(url) as res: + if res.status > 299: + raise HTTPException() + + def start(self) -> "VaultContainer": + super().start() + self._healthcheck() + return self diff --git a/modules/vault/tests/test_vault.py b/modules/vault/tests/test_vault.py new file mode 100644 index 000000000..54017d2f9 --- /dev/null +++ b/modules/vault/tests/test_vault.py @@ -0,0 +1,41 @@ +import hvac +from testcontainers.vault import VaultContainer + + +def test_docker_run_vault(): + config = VaultContainer("hashicorp/vault:1.16.1") + with config as vault: + url = vault.get_connection_url() + client = hvac.Client(url=url) + status = client.sys.read_health_status() + assert status.status_code == 200 + + +def test_docker_run_vault_act_as_root(): + config = VaultContainer("hashicorp/vault:1.16.1") + with config as vault: + url = vault.get_connection_url() + client = hvac.Client(url=url, token=vault.root_token) + assert client.is_authenticated() + assert client.sys.is_initialized() + assert not client.sys.is_sealed() + + client.sys.enable_secrets_engine( + backend_type="kv", + path="secrets", + config={ + "version": "2", + }, + ) + client.secrets.kv.v2.create_or_update_secret( + path="my-secret", + mount_point="secrets", + secret={ + "pssst": "this is secret", + }, + ) + resp = client.secrets.kv.v2.read_secret( + path="my-secret", + mount_point="secrets", + ) + assert resp["data"]["data"]["pssst"] == "this is secret" diff --git a/poetry.lock b/poetry.lock index 614e42e5f..a2f81d3b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1449,6 +1449,23 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[[package]] +name = "hvac" +version = "2.1.0" +description = "HashiCorp Vault API client" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "hvac-2.1.0-py3-none-any.whl", hash = "sha256:73bc91e58c3fc7c6b8107cdaca9cb71fa0a893dfd80ffbc1c14e20f24c0c29d7"}, + {file = "hvac-2.1.0.tar.gz", hash = "sha256:b48bcda11a4ab0a7b6c47232c7ba7c87fda318ae2d4a7662800c465a78742894"}, +] + +[package.dependencies] +requests = ">=2.27.1,<3.0.0" + +[package.extras] +parser = ["pyhcl (>=0.4.4,<0.5.0)"] + [[package]] name = "hyperframe" version = "6.0.1" @@ -4188,9 +4205,10 @@ rabbitmq = ["pika"] redis = ["redis"] registry = ["bcrypt"] selenium = ["selenium"] +vault = [] weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "af9f21cb52ebd761ba91c852c9839d982124c149e77abdc079fc5657cecb9ff7" +content-hash = "233dfd72d07a555973aafc3fe3b6676574403b9fe4bb2c0230d455cff8aa2933" diff --git a/pyproject.toml b/pyproject.toml index 2d2fbeb55..08c9c68bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ packages = [ { include = "testcontainers", from = "modules/redis" }, { include = "testcontainers", from = "modules/registry" }, { include = "testcontainers", from = "modules/selenium" }, + { include = "testcontainers", from = "modules/vault" }, { include = "testcontainers", from = "modules/weaviate" } ] @@ -125,6 +126,7 @@ rabbitmq = ["pika"] redis = ["redis"] registry = ["bcrypt"] selenium = ["selenium"] +vault = [] weaviate = ["weaviate-client"] chroma = ["chromadb-client"] @@ -144,6 +146,7 @@ psycopg = "*" cassandra-driver = "*" pytest-asyncio = "0.23.5" kafka-python-ng = "^2.2.0" +hvac = "*" [[tool.poetry.source]] name = "PyPI" @@ -262,6 +265,7 @@ mypy_path = [ # "modules/rabbitmq", # "modules/redis", # "modules/selenium" +# "modules/vault" # "modules/weaviate" ] enable_error_code = [ From 065f7077ea51971f3e00595375a42fea4405eb0e Mon Sep 17 00:00:00 2001 From: David Ankin Date: Mon, 8 Apr 2024 05:24:39 -0400 Subject: [PATCH 11/15] fix(core): make config editable to avoid monkeypatching.1 (#532) see #531: I am using testcontainers within a library that provides some pytest-fixtures. In order for this to work I have change some settings. As I can not guarantee that that my lib is imported before testcontainers I need to monkeypatch the settings. This is much easier if I only need to monkeypatch the config file and not all modules that use configurations. I would argue that for a potential library as this, this is a better design. Also one can easier see that the given UPERCASE variable is not a constant but rather a setting. Co-authored-by: Carli* Freudenberg --- core/testcontainers/core/config.py | 61 +++++++++++++++++++ core/testcontainers/core/container.py | 18 ++---- core/testcontainers/core/docker_client.py | 28 +-------- core/testcontainers/core/labels.py | 4 +- core/testcontainers/core/waiting_utils.py | 10 +-- core/tests/test_ryuk.py | 6 +- .../testcontainers/arangodb/__init__.py | 4 +- modules/k3s/testcontainers/k3s/__init__.py | 4 +- .../neo4j/testcontainers/neo4j/__init__.py | 4 +- .../testcontainers/postgres/__init__.py | 8 +-- .../qdrant/testcontainers/qdrant/__init__.py | 4 +- 11 files changed, 91 insertions(+), 60 deletions(-) diff --git a/core/testcontainers/core/config.py b/core/testcontainers/core/config.py index 0c1b5e0c2..391c88bfa 100644 --- a/core/testcontainers/core/config.py +++ b/core/testcontainers/core/config.py @@ -1,4 +1,7 @@ +from dataclasses import dataclass, field from os import environ +from os.path import exists +from pathlib import Path MAX_TRIES = int(environ.get("TC_MAX_TRIES", 120)) SLEEP_TIME = int(environ.get("TC_POOLING_INTERVAL", 1)) @@ -9,3 +12,61 @@ 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") + +TC_FILE = ".testcontainers.properties" +TC_GLOBAL = Path.home() / TC_FILE + + +def read_tc_properties() -> dict[str, str]: + """ + Read the .testcontainers.properties for settings. (see the Java implementation for details) + Currently we only support the ~/.testcontainers.properties but may extend to per-project variables later. + + :return: the merged properties from the sources. + """ + tc_files = [item for item in [TC_GLOBAL] if exists(item)] + if not tc_files: + return {} + settings = {} + + for file in tc_files: + with open(file) as contents: + tuples = [line.split("=") for line in contents.readlines() if "=" in line] + settings = {**settings, **{item[0].strip(): item[1].strip() for item in tuples}} + return settings + + +@dataclass +class TestcontainersConfiguration: + max_tries: int = MAX_TRIES + sleep_time: int = SLEEP_TIME + ryuk_image: str = RYUK_IMAGE + ryuk_privileged: bool = RYUK_PRIVILEGED + ryuk_disabled: bool = RYUK_DISABLED + ryuk_docker_socket: str = RYUK_DOCKER_SOCKET + ryuk_reconnection_timeout: str = RYUK_RECONNECTION_TIMEOUT + tc_properties: dict[str, str] = field(default_factory=read_tc_properties) + + def tc_properties_get_tc_host(self): + return self.tc_properties.get("tc.host") + + @property + def timeout(self): + return self.max_tries * self.sleep_time + + +testcontainers_config = TestcontainersConfiguration() + +__all__ = [ + # the public API of this module + "testcontainers_config", + # and all the legacy things that are deprecated: + "MAX_TRIES", + "SLEEP_TIME", + "TIMEOUT", + "RYUK_IMAGE", + "RYUK_PRIVILEGED", + "RYUK_DISABLED", + "RYUK_DOCKER_SOCKET", + "RYUK_RECONNECTION_TIMEOUT", +] diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index 639980440..d2605490b 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -6,13 +6,7 @@ import docker.errors from typing_extensions import Self -from testcontainers.core.config import ( - RYUK_DISABLED, - RYUK_DOCKER_SOCKET, - RYUK_IMAGE, - RYUK_PRIVILEGED, - RYUK_RECONNECTION_TIMEOUT, -) +from testcontainers.core.config import testcontainers_config as c from testcontainers.core.docker_client import DockerClient from testcontainers.core.exceptions import ContainerStartException from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID @@ -88,7 +82,7 @@ def maybe_emulate_amd64(self) -> Self: return self def start(self) -> Self: - if not RYUK_DISABLED and self.image != RYUK_IMAGE: + if not c.ryuk_disabled and self.image != c.ryuk_image: logger.debug("Creating Ryuk container") Reaper.get_instance() logger.info("Pulling image %s", self.image) @@ -214,12 +208,12 @@ def _create_instance(cls) -> "Reaper": logger.debug(f"Creating new Reaper for session: {SESSION_ID}") Reaper._container = ( - DockerContainer(RYUK_IMAGE) + DockerContainer(c.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, auto_remove=True) - .with_env("RYUK_RECONNECTION_TIMEOUT", RYUK_RECONNECTION_TIMEOUT) + .with_volume_mapping(c.ryuk_docker_socket, "/var/run/docker.sock", "rw") + .with_kwargs(privileged=c.ryuk_privileged, auto_remove=True) + .with_env("RYUK_RECONNECTION_TIMEOUT", c.ryuk_reconnection_timeout) .start() ) wait_for_logs(Reaper._container, r".* Started!") diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 89db0fbfc..9ff6170e6 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -16,20 +16,17 @@ import os import urllib import urllib.parse -from os.path import exists -from pathlib import Path from typing import Callable, Optional, TypeVar, Union import docker from docker.models.containers import Container, ContainerCollection from typing_extensions import ParamSpec +from testcontainers.core.config import testcontainers_config as c 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 _P = ParamSpec("_P") _T = TypeVar("_T") @@ -185,26 +182,5 @@ def host(self) -> str: return "localhost" -@ft.cache -def read_tc_properties() -> dict[str, str]: - """ - Read the .testcontainers.properties for settings. (see the Java implementation for details) - Currently we only support the ~/.testcontainers.properties but may extend to per-project variables later. - - :return: the merged properties from the sources. - """ - tc_files = [item for item in [TC_GLOBAL] if exists(item)] - if not tc_files: - return {} - settings = {} - - for file in tc_files: - tuples = [] - with open(file) as contents: - tuples = [line.split("=") for line in contents.readlines() if "=" in line] - settings = {**settings, **{item[0].strip(): item[1].strip() for item in tuples}} - return settings - - def get_docker_host() -> Optional[str]: - return read_tc_properties().get("tc.host") or os.getenv("DOCKER_HOST") + return c.tc_properties_get_tc_host() or os.getenv("DOCKER_HOST") diff --git a/core/testcontainers/core/labels.py b/core/testcontainers/core/labels.py index 13937a5e8..144e4365e 100644 --- a/core/testcontainers/core/labels.py +++ b/core/testcontainers/core/labels.py @@ -1,7 +1,7 @@ from typing import Optional from uuid import uuid4 -from testcontainers.core.config import RYUK_IMAGE +from testcontainers.core.config import testcontainers_config as c SESSION_ID: str = str(uuid4()) LABEL_SESSION_ID = "org.testcontainers.session-id" @@ -13,7 +13,7 @@ def create_labels(image: str, labels: Optional[dict[str, str]]) -> dict[str, str labels = {} labels[LABEL_LANG] = "python" - if image == RYUK_IMAGE: + if image == c.ryuk_image: return labels labels[LABEL_SESSION_ID] = SESSION_ID diff --git a/core/testcontainers/core/waiting_utils.py b/core/testcontainers/core/waiting_utils.py index ea52683d5..4eb7ad890 100644 --- a/core/testcontainers/core/waiting_utils.py +++ b/core/testcontainers/core/waiting_utils.py @@ -19,7 +19,7 @@ import wrapt -from testcontainers.core import config +from testcontainers.core.config import testcontainers_config as config from testcontainers.core.utils import setup_logger if TYPE_CHECKING: @@ -54,18 +54,18 @@ def wrapper(wrapped: Callable, instance: Any, args: list, kwargs: dict) -> Any: logger.info("Waiting for %s to be ready ...", instance) exception = None - for attempt_no in range(config.MAX_TRIES): + for attempt_no in range(config.max_tries): try: return wrapped(*args, **kwargs) except transient_exceptions as e: logger.debug( - f"Connection attempt '{attempt_no + 1}' of '{config.MAX_TRIES + 1}' " + f"Connection attempt '{attempt_no + 1}' of '{config.max_tries + 1}' " f"failed: {traceback.format_exc()}" ) - time.sleep(config.SLEEP_TIME) + time.sleep(config.sleep_time) exception = e raise TimeoutError( - f"Wait time ({config.TIMEOUT}s) exceeded for {wrapped.__name__}(args: {args}, kwargs: " + f"Wait time ({config.timeout}s) exceeded for {wrapped.__name__}(args: {args}, kwargs: " f"{kwargs}). Exception: {exception}" ) diff --git a/core/tests/test_ryuk.py b/core/tests/test_ryuk.py index e21b045ba..e081d2c07 100644 --- a/core/tests/test_ryuk.py +++ b/core/tests/test_ryuk.py @@ -5,7 +5,7 @@ from docker import DockerClient from docker.errors import NotFound -from testcontainers.core import container as container_module +from testcontainers.core.config import testcontainers_config from testcontainers.core.container import Reaper from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs @@ -13,7 +13,7 @@ def test_wait_for_reaper(monkeypatch: MonkeyPatch): Reaper.delete_instance() - monkeypatch.setattr(container_module, "RYUK_RECONNECTION_TIMEOUT", "0.1s") + monkeypatch.setattr(testcontainers_config, "ryuk_reconnection_timeout", "0.1s") docker_client = DockerClient() container = DockerContainer("hello-world").start() @@ -40,7 +40,7 @@ def test_wait_for_reaper(monkeypatch: MonkeyPatch): def test_container_without_ryuk(monkeypatch: MonkeyPatch): Reaper.delete_instance() - monkeypatch.setattr(container_module, "RYUK_DISABLED", True) + monkeypatch.setattr(testcontainers_config, "ryuk_disabled", True) with DockerContainer("hello-world") as container: wait_for_logs(container, "Hello from Docker!") assert Reaper._instance is None diff --git a/modules/arangodb/testcontainers/arangodb/__init__.py b/modules/arangodb/testcontainers/arangodb/__init__.py index a7c954652..9ea36f6ea 100644 --- a/modules/arangodb/testcontainers/arangodb/__init__.py +++ b/modules/arangodb/testcontainers/arangodb/__init__.py @@ -5,7 +5,7 @@ import typing from os import environ -from testcontainers.core.config import TIMEOUT +from testcontainers.core.config import testcontainers_config as c from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter from testcontainers.core.waiting_utils import wait_for_logs @@ -90,4 +90,4 @@ def get_connection_url(self) -> str: return f"http://{self.get_container_host_ip()}:{port}" def _connect(self) -> None: - wait_for_logs(self, predicate="is ready for business", timeout=TIMEOUT) + wait_for_logs(self, predicate="is ready for business", timeout=c.timeout) diff --git a/modules/k3s/testcontainers/k3s/__init__.py b/modules/k3s/testcontainers/k3s/__init__.py index 045e2eb5d..2682df356 100644 --- a/modules/k3s/testcontainers/k3s/__init__.py +++ b/modules/k3s/testcontainers/k3s/__init__.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from testcontainers.core.config import MAX_TRIES +from testcontainers.core.config import testcontainers_config from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for_logs @@ -46,7 +46,7 @@ def __init__(self, image="rancher/k3s:latest", **kwargs) -> None: self.with_volume_mapping("/sys/fs/cgroup", "/sys/fs/cgroup", "rw") def _connect(self) -> None: - wait_for_logs(self, predicate="Node controller sync successful", timeout=MAX_TRIES) + wait_for_logs(self, predicate="Node controller sync successful", timeout=testcontainers_config.timeout) def start(self) -> "K3SContainer": super().start() diff --git a/modules/neo4j/testcontainers/neo4j/__init__.py b/modules/neo4j/testcontainers/neo4j/__init__.py index 26f46dc61..7939c013f 100644 --- a/modules/neo4j/testcontainers/neo4j/__init__.py +++ b/modules/neo4j/testcontainers/neo4j/__init__.py @@ -15,7 +15,7 @@ from typing import Optional from neo4j import Driver, GraphDatabase -from testcontainers.core.config import TIMEOUT +from testcontainers.core.config import testcontainers_config as c from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs @@ -62,7 +62,7 @@ def get_connection_url(self) -> str: @wait_container_is_ready() def _connect(self) -> None: - wait_for_logs(self, "Remote interface available at", TIMEOUT) + wait_for_logs(self, "Remote interface available at", c.timeout) # Then we actually check that the container really is listening with self.get_driver() as driver: diff --git a/modules/postgres/testcontainers/postgres/__init__.py b/modules/postgres/testcontainers/postgres/__init__.py index 3810ea0f2..9b347aa61 100644 --- a/modules/postgres/testcontainers/postgres/__init__.py +++ b/modules/postgres/testcontainers/postgres/__init__.py @@ -14,7 +14,7 @@ from time import sleep from typing import Optional -from testcontainers.core.config import MAX_TRIES, SLEEP_TIME +from testcontainers.core.config import testcontainers_config as c from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs @@ -91,15 +91,15 @@ def get_connection_url(self, host: Optional[str] = None, driver: Optional[str] = @wait_container_is_ready() def _connect(self) -> None: - wait_for_logs(self, ".*database system is ready to accept connections.*", MAX_TRIES, SLEEP_TIME) + wait_for_logs(self, ".*database system is ready to accept connections.*", c.max_tries, c.sleep_time) count = 0 - while count < MAX_TRIES: + while count < c.max_tries: status, _ = self.exec(f"pg_isready -hlocalhost -p{self.port} -U{self.username}") if status == 0: return - sleep(SLEEP_TIME) + sleep(c.sleep_time) count += 1 raise RuntimeError("Postgres could not get into a ready state") diff --git a/modules/qdrant/testcontainers/qdrant/__init__.py b/modules/qdrant/testcontainers/qdrant/__init__.py index ac9279955..d36fe62ee 100644 --- a/modules/qdrant/testcontainers/qdrant/__init__.py +++ b/modules/qdrant/testcontainers/qdrant/__init__.py @@ -15,7 +15,7 @@ from pathlib import Path from typing import Optional -from testcontainers.core.config import TIMEOUT +from testcontainers.core.config import testcontainers_config as c from testcontainers.core.generic import DbContainer from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs @@ -61,7 +61,7 @@ def _configure(self) -> None: @wait_container_is_ready() def _connect(self) -> None: - wait_for_logs(self, ".*Actix runtime found; starting in Actix runtime.*", TIMEOUT) + wait_for_logs(self, ".*Actix runtime found; starting in Actix runtime.*", c.timeout) def get_client(self, **kwargs): """ From ba33506cb2e8c539aed6082fe6fc4d89777d9df7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 05:27:17 -0400 Subject: [PATCH 12/15] chore(main): release testcontainers 4.3.2 (#530) :robot: I have created a release *beep* *boop* --- ## [4.3.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.3.1...testcontainers-v4.3.2) (2024-04-08) ### Bug Fixes * **core:** Improve typing for common container usage scenarios ([#523](https://github.com/testcontainers/testcontainers-python/issues/523)) ([d5b8553](https://github.com/testcontainers/testcontainers-python/commit/d5b855323be06f8d1395dd480a347f0efef75703)) * **core:** make config editable to avoid monkeypatching.1 ([#532](https://github.com/testcontainers/testcontainers-python/issues/532)) ([3be6da3](https://github.com/testcontainers/testcontainers-python/commit/3be6da335ba2026b4800dfd6a19cda4ca8e52be8)) * **vault:** add support for HashiCorp Vault container ([#366](https://github.com/testcontainers/testcontainers-python/issues/366)) ([1326278](https://github.com/testcontainers/testcontainers-python/commit/13262785dedf32a97e392afc1a758616995dc9d9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/.release-please-manifest.json | 2 +- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index fa1ad28ce..b2b1cb883 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.3.1" + ".": "4.3.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 761b2b2cc..b7a007b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [4.3.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.3.1...testcontainers-v4.3.2) (2024-04-08) + + +### Bug Fixes + +* **core:** Improve typing for common container usage scenarios ([#523](https://github.com/testcontainers/testcontainers-python/issues/523)) ([d5b8553](https://github.com/testcontainers/testcontainers-python/commit/d5b855323be06f8d1395dd480a347f0efef75703)) +* **core:** make config editable to avoid monkeypatching.1 ([#532](https://github.com/testcontainers/testcontainers-python/issues/532)) ([3be6da3](https://github.com/testcontainers/testcontainers-python/commit/3be6da335ba2026b4800dfd6a19cda4ca8e52be8)) +* **vault:** add support for HashiCorp Vault container ([#366](https://github.com/testcontainers/testcontainers-python/issues/366)) ([1326278](https://github.com/testcontainers/testcontainers-python/commit/13262785dedf32a97e392afc1a758616995dc9d9)) + ## [4.3.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.3.0...testcontainers-v4.3.1) (2024-04-02) diff --git a/pyproject.toml b/pyproject.toml index 08c9c68bc..59e94e162 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "testcontainers" -version = "4.3.1" # auto-incremented by release-please +version = "4.3.2" # auto-incremented by release-please description = "Python library for throwaway instances of anything that can run in a Docker container" authors = ["Sergey Pirogov "] maintainers = [ From 9db1080c416c26f9cb4f542befe19dd17958ffa1 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Mon, 8 Apr 2024 11:39:20 +0200 Subject: [PATCH 13/15] chore: pin nginx image --- core/tests/test_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tests/test_network.py b/core/tests/test_network.py index 2e490a4e6..e8325583d 100644 --- a/core/tests/test_network.py +++ b/core/tests/test_network.py @@ -2,7 +2,7 @@ from testcontainers.core.docker_client import DockerClient from testcontainers.core.network import Network -NGINX_ALPINE_SLIM_IMAGE = "nginx:alpine-slim" +NGINX_ALPINE_SLIM_IMAGE = "nginx:1.25.4-alpine-slim" def test_network_gets_created_and_cleaned_up(): From 94262549a5837e470a2a6153eaf48b22ca883bd2 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Mon, 8 Apr 2024 11:45:57 +0200 Subject: [PATCH 14/15] fix class style --- core/testcontainers/core/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index 9ff1e843b..ebd70ac24 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -16,7 +16,7 @@ from testcontainers.core.docker_client import DockerClient -class Network(object): +class Network: """ Network context manager for programmatically connecting containers. """ From 2e50cb3fbc8de6ffa807d9b43d67d60c25f3b7e7 Mon Sep 17 00:00:00 2001 From: Mathias Loesch Date: Tue, 16 Apr 2024 09:44:06 +0200 Subject: [PATCH 15/15] chore: reformat --- core/testcontainers/core/network.py | 2 +- core/tests/test_network.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/testcontainers/core/network.py b/core/testcontainers/core/network.py index ebd70ac24..9903d0710 100644 --- a/core/testcontainers/core/network.py +++ b/core/testcontainers/core/network.py @@ -32,7 +32,7 @@ def connect(self, container_id: str, network_aliases: Optional[list] = None): def remove(self) -> None: self._network.remove() - def __enter__(self) -> 'Network': + def __enter__(self) -> "Network": self._network = self._docker.client.networks.create(self.name, **self._docker_network_kw) self.id = self._network.id return self diff --git a/core/tests/test_network.py b/core/tests/test_network.py index e8325583d..4b0764d4d 100644 --- a/core/tests/test_network.py +++ b/core/tests/test_network.py @@ -16,15 +16,18 @@ def test_network_gets_created_and_cleaned_up(): def test_containers_can_communicate_over_network(): with Network() as network: - with DockerContainer(NGINX_ALPINE_SLIM_IMAGE)\ - .with_name("alpine1")\ - .with_network_aliases("alpine1-alias-1", "alpine1-alias-2")\ - .with_network(network) as alpine1: - with DockerContainer(NGINX_ALPINE_SLIM_IMAGE)\ - .with_name("alpine2")\ - .with_network_aliases("alpine2-alias-1", "alpine2-alias-2")\ - .with_network(network) as alpine2: - + with ( + DockerContainer(NGINX_ALPINE_SLIM_IMAGE) + .with_name("alpine1") + .with_network_aliases("alpine1-alias-1", "alpine1-alias-2") + .with_network(network) as alpine1 + ): + with ( + DockerContainer(NGINX_ALPINE_SLIM_IMAGE) + .with_name("alpine2") + .with_network_aliases("alpine2-alias-1", "alpine2-alias-2") + .with_network(network) as alpine2 + ): assert_can_ping(alpine1, "alpine2") assert_can_ping(alpine1, "alpine2-alias-1") assert_can_ping(alpine1, "alpine2-alias-2")