Skip to content

Commit 11964de

Browse files
mloeschkiviewmax-pfeifferf4z3ralexanderankin
authored
feat(network): Add network context manager (#367)
This PR adds a `Network` helper class that allows to create networks and connect containers programmatically. The networks are context-managed resources like containers created via `DockerContainer`. Please also see tests for a usage example :) --------- Co-authored-by: Kevin Wittek <[email protected]> Co-authored-by: Max Pfeiffer <[email protected]> Co-authored-by: Jakob Beckmann <[email protected]> Co-authored-by: David Ankin <[email protected]> Co-authored-by: Carli* Freudenberg <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Vemund Santi <[email protected]>
1 parent 29b5179 commit 11964de

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

core/testcontainers/core/container.py

+13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from testcontainers.core.docker_client import DockerClient
1111
from testcontainers.core.exceptions import ContainerStartException
1212
from testcontainers.core.labels import LABEL_SESSION_ID, SESSION_ID
13+
from testcontainers.core.network import Network
1314
from testcontainers.core.utils import inside_container, is_arm, setup_logger
1415
from testcontainers.core.waiting_utils import wait_container_is_ready, wait_for_logs
1516

@@ -46,6 +47,8 @@ def __init__(
4647
self._container = None
4748
self._command = None
4849
self._name = None
50+
self._network: Optional[Network] = None
51+
self._network_aliases: Optional[list[str]] = None
4952
self._kwargs = kwargs
5053

5154
def with_env(self, key: str, value: str) -> Self:
@@ -61,6 +64,14 @@ def with_exposed_ports(self, *ports: int) -> Self:
6164
self.ports[port] = None
6265
return self
6366

67+
def with_network(self, network: Network) -> Self:
68+
self._network = network
69+
return self
70+
71+
def with_network_aliases(self, *aliases) -> Self:
72+
self._network_aliases = aliases
73+
return self
74+
6475
def with_kwargs(self, **kwargs) -> Self:
6576
self._kwargs = kwargs
6677
return self
@@ -87,6 +98,8 @@ def start(self) -> Self:
8798
**self._kwargs,
8899
)
89100
logger.info("Container started: %s", self._container.short_id)
101+
if self._network:
102+
self._network.connect(self._container.id, self._network_aliases)
90103
return self
91104

92105
def stop(self, force=True, delete_volume=True) -> None:

core/testcontainers/core/network.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#
2+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
3+
# not use this file except in compliance with the License. You may obtain
4+
# a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
# License for the specific language governing permissions and limitations
12+
# under the License.
13+
import uuid
14+
from typing import Optional
15+
16+
from testcontainers.core.docker_client import DockerClient
17+
18+
19+
class Network:
20+
"""
21+
Network context manager for programmatically connecting containers.
22+
"""
23+
24+
def __init__(self, docker_client_kw: Optional[dict] = None, docker_network_kw: Optional[dict] = None) -> None:
25+
self.name = str(uuid.uuid4())
26+
self._docker = DockerClient(**(docker_client_kw or {}))
27+
self._docker_network_kw = docker_network_kw or {}
28+
29+
def connect(self, container_id: str, network_aliases: Optional[list] = None):
30+
self._network.connect(container_id, aliases=network_aliases)
31+
32+
def remove(self) -> None:
33+
self._network.remove()
34+
35+
def __enter__(self) -> "Network":
36+
self._network = self._docker.client.networks.create(self.name, **self._docker_network_kw)
37+
self.id = self._network.id
38+
return self
39+
40+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
41+
self.remove()

core/tests/test_network.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from testcontainers.core.container import DockerContainer
2+
from testcontainers.core.docker_client import DockerClient
3+
from testcontainers.core.network import Network
4+
5+
NGINX_ALPINE_SLIM_IMAGE = "nginx:1.25.4-alpine-slim"
6+
7+
8+
def test_network_gets_created_and_cleaned_up():
9+
with Network() as network:
10+
docker = DockerClient()
11+
networks_list = docker.client.networks.list(network.name)
12+
assert networks_list[0].name == network.name
13+
assert networks_list[0].id == network.id
14+
assert not docker.client.networks.list(network.name)
15+
16+
17+
def test_containers_can_communicate_over_network():
18+
with Network() as network:
19+
with (
20+
DockerContainer(NGINX_ALPINE_SLIM_IMAGE)
21+
.with_name("alpine1")
22+
.with_network_aliases("alpine1-alias-1", "alpine1-alias-2")
23+
.with_network(network) as alpine1
24+
):
25+
with (
26+
DockerContainer(NGINX_ALPINE_SLIM_IMAGE)
27+
.with_name("alpine2")
28+
.with_network_aliases("alpine2-alias-1", "alpine2-alias-2")
29+
.with_network(network) as alpine2
30+
):
31+
assert_can_ping(alpine1, "alpine2")
32+
assert_can_ping(alpine1, "alpine2-alias-1")
33+
assert_can_ping(alpine1, "alpine2-alias-2")
34+
35+
assert_can_ping(alpine2, "alpine1")
36+
assert_can_ping(alpine2, "alpine1-alias-1")
37+
assert_can_ping(alpine2, "alpine1-alias-2")
38+
39+
40+
def assert_can_ping(container: DockerContainer, remote_name: str):
41+
status, output = container.exec("ping -c 1 %s" % remote_name)
42+
assert status == 0
43+
assert "64 bytes" in str(output)

0 commit comments

Comments
 (0)