Skip to content

Commit 146b618

Browse files
committed
feat(azurite): Enable robust inter-container communication
Add functionality to generate Azurite connection strings suitable for direct container-to-container communication within a Docker network, alongside existing local host access. Key changes include: - Introduces `ConnectionStringType` enum to provide distinct Azurite connection strings for different access patterns: - `NETWORK`: Optimized for inter-container communication using network aliases. - `LOCALHOST`: For access from the host machine via exposed ports. - Refactored `get_connection_string` to dispatch based on `ConnectionStringType`. - Improved `get_external_connection_string` to prioritize network aliases nd provide a fallback to host IP. - Added comprehensive unit test (`test_azurite_inter_container_communication_with_network_string`) to verify inter-container connectivity.
1 parent fc4155e commit 146b618

File tree

4 files changed

+228
-2
lines changed

4 files changed

+228
-2
lines changed

modules/azurite/testcontainers/azurite/__init__.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
13+
import enum
1314
import os
1415
import socket
1516
from typing import Optional
@@ -19,6 +20,20 @@
1920
from testcontainers.core.waiting_utils import wait_container_is_ready
2021

2122

23+
class ConnectionStringType(enum.Enum):
24+
"""
25+
Enumeration for specifying the type of connection string to generate for Azurite.
26+
27+
:cvar LOCALHOST: Represents a connection string for access from the host machine
28+
where the tests are running.
29+
:cvar NETWORK: Represents a connection string for access from another container
30+
within the same Docker network as the Azurite container.
31+
"""
32+
33+
LOCALHOST = "localhost"
34+
NETWORK = "network"
35+
36+
2237
class AzuriteContainer(DockerContainer):
2338
"""
2439
The example below spins up an Azurite container and
@@ -73,7 +88,46 @@ def __init__(
7388
self.with_exposed_ports(blob_service_port, queue_service_port, table_service_port)
7489
self.with_env("AZURITE_ACCOUNTS", f"{self.account_name}:{self.account_key}")
7590

76-
def get_connection_string(self) -> str:
91+
def get_connection_string(
92+
self, connection_string_type: ConnectionStringType = ConnectionStringType.LOCALHOST
93+
) -> str:
94+
"""Retrieves the appropriate connection string for the Azurite container based on the specified access type.
95+
96+
This method acts as a dispatcher, returning a connection string optimized
97+
either for access from the host machine or for inter-container communication within the same Docker network.
98+
99+
:param connection_string_type: The type of connection string to generate.
100+
Use :attr:`ConnectionStringType.LOCALHOST` for connections
101+
from the machine running the tests (default), or
102+
:attr:`ConnectionStringType.NETWORK` for connections
103+
from other containers within the same Docker network.
104+
:type connection_string_type: ConnectionStringType
105+
:return: The generated Azurite connection string.
106+
:rtype: str
107+
:raises ValueError: If an unrecognized `connection_string_type` is provided.
108+
"""
109+
match connection_string_type:
110+
case ConnectionStringType.LOCALHOST:
111+
return self.__get_local_connection_string()
112+
case ConnectionStringType.NETWORK:
113+
return self.__get_external_connection_string()
114+
case _:
115+
raise ValueError(
116+
f"unrecognized connection string type {connection_string_type}, "
117+
f"Supported values are ConnectionStringType.LOCALHOST or ConnectionStringType.NETWORK "
118+
)
119+
120+
def __get_local_connection_string(self) -> str:
121+
"""Generates a connection string for Azurite accessible from the local host machine.
122+
123+
This connection string uses the Docker host IP address (obtained via
124+
:meth:`testcontainers.core.container.DockerContainer.get_container_host_ip`)
125+
and the dynamically exposed ports of the Azurite container. This ensures that
126+
clients running on the host can connect successfully to the Azurite services.
127+
128+
:return: The Azurite connection string for local host access.
129+
:rtype: str
130+
"""
77131
host_ip = self.get_container_host_ip()
78132
connection_string = (
79133
f"DefaultEndpointsProtocol=http;AccountName={self.account_name};AccountKey={self.account_key};"
@@ -96,6 +150,75 @@ def get_connection_string(self) -> str:
96150

97151
return connection_string
98152

153+
def __get_external_connection_string(self) -> str:
154+
"""Generates a connection string for Azurite, primarily optimized for
155+
inter-container communication within a custom Docker network.
156+
157+
This method attempts to provide the most suitable connection string
158+
based on the container's network configuration:
159+
160+
- **For Inter-Container Communication (Recommended):** If the Azurite container is
161+
part of a custom Docker network and has network aliases configured,
162+
the connection string will use the first network alias as the hostname
163+
and the internal container ports (e.g., #$#`http://<alias>:<internal_port>/<account_name>`#$#).
164+
This is the most efficient and robust way for other containers
165+
in the same network to connect to Azurite, leveraging Docker's internal DNS.
166+
167+
- **Fallback for Non-Networked/Aliased Scenarios:** If the container is
168+
not on a custom network with aliases (e.g., running on the default
169+
bridge network without explicit aliases), the method falls back to
170+
using the Docker host IP (obtained via
171+
:meth:`testcontainers.core.container.DockerContainer.get_container_host_ip`)
172+
and the dynamically exposed ports (e.g., #$#`http://<host_ip>:<exposed_port>/<account_name>`#$#).
173+
While this connection string is technically "external" to the container,
174+
it primarily facilitates connections *from the host machine*.
175+
176+
:return: The generated Azurite connection string.
177+
:rtype: str
178+
"""
179+
# Check if we're on a custom network and have network aliases
180+
if hasattr(self, "_network") and self._network and hasattr(self, "_network_aliases") and self._network_aliases:
181+
# Use the first network alias for inter-container communication
182+
host_ip = self._network_aliases[0]
183+
# When using network aliases, use the internal container ports
184+
blob_port = self.blob_service_port
185+
queue_port = self.queue_service_port
186+
table_port = self.table_service_port
187+
else:
188+
# Use the Docker host IP for external connections
189+
host_ip = self.get_container_host_ip()
190+
# When using host IP, use the exposed ports
191+
blob_port = (
192+
self.get_exposed_port(self.blob_service_port)
193+
if self.blob_service_port in self.ports
194+
else self.blob_service_port
195+
)
196+
queue_port = (
197+
self.get_exposed_port(self.queue_service_port)
198+
if self.queue_service_port in self.ports
199+
else self.queue_service_port
200+
)
201+
table_port = (
202+
self.get_exposed_port(self.table_service_port)
203+
if self.table_service_port in self.ports
204+
else self.table_service_port
205+
)
206+
207+
connection_string = (
208+
f"DefaultEndpointsProtocol=http;AccountName={self.account_name};AccountKey={self.account_key};"
209+
)
210+
211+
if self.blob_service_port in self.ports:
212+
connection_string += f"BlobEndpoint=http://{host_ip}:{blob_port}/{self.account_name};"
213+
214+
if self.queue_service_port in self.ports:
215+
connection_string += f"QueueEndpoint=http://{host_ip}:{queue_port}/{self.account_name};"
216+
217+
if self.table_service_port in self.ports:
218+
connection_string += f"TableEndpoint=http://{host_ip}:{table_port}/{self.account_name};"
219+
220+
return connection_string
221+
99222
def start(self) -> "AzuriteContainer":
100223
super().start()
101224
self._connect()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Use an official Python runtime as a parent image
2+
FROM python:3.10-slim
3+
4+
# Set the working directory in the container
5+
WORKDIR /app
6+
7+
RUN pip install azure-storage-blob==12.19.0
8+
9+
COPY ./netowrk_container.py netowrk_container.py
10+
EXPOSE 80
11+
# Define the command to run the application
12+
CMD ["python", "netowrk_container.py"]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from azure.storage.blob import BlobClient, BlobServiceClient
2+
import os
3+
4+
5+
def hello_from_external_container():
6+
"""
7+
Entry point function for a custom Docker container to test connectivity
8+
and operations with Azurite (or Azure Blob Storage).
9+
10+
This function is designed to run inside a separate container within the
11+
same Docker network as an Azurite instance. It retrieves connection
12+
details from environment variables and attempts to create a new
13+
blob container on the connected storage account.
14+
"""
15+
connection_string = os.environ["AZURE_CONNECTION_STRING"]
16+
container_to_create = os.environ["AZURE_CONTAINER"]
17+
blob_service_client = BlobServiceClient.from_connection_string(connection_string)
18+
# create dummy container just to make sure we can process the
19+
try:
20+
blob_service_client.create_container(name=container_to_create)
21+
print("Azure Storage Container created.")
22+
except Exception as e:
23+
print(f"Something went wrong : {e}")
24+
25+
26+
if __name__ == "__main__":
27+
hello_from_external_container()

modules/azurite/tests/test_azurite.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1+
import logging
2+
import time
3+
from pathlib import Path
4+
15
from azure.storage.blob import BlobServiceClient
26

3-
from testcontainers.azurite import AzuriteContainer
7+
from testcontainers.azurite import AzuriteContainer, ConnectionStringType
8+
9+
from testcontainers.core.image import DockerImage
10+
from testcontainers.core.container import DockerContainer
11+
from testcontainers.core.network import Network
12+
from testcontainers.core.waiting_utils import wait_for_logs
13+
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
DOCKER_FILE_PATH = ".modules/azurite/tests/external_container_sample"
19+
IMAGE_TAG = "external_container:test"
20+
21+
TEST_DIR = Path(__file__).parent
422

523

624
def test_docker_run_azurite():
@@ -10,3 +28,49 @@ def test_docker_run_azurite():
1028
)
1129

1230
blob_service_client.create_container("test-container")
31+
32+
33+
def test_docker_run_azurite_inter_container_communication():
34+
"""Tests inter-container communication between an Azurite container and a custom
35+
application container within the same Docker network, while also verifying
36+
local machine access to Azurite.
37+
38+
This test case validates the following:
39+
1. An Azurite container can be successfully started and configured with a
40+
custom Docker network and a network alias.
41+
2. A custom application container can connect to the Azurite container
42+
using a network-specific connection string (via its network alias)
43+
within the shared Docker network.
44+
3. The Azurite container remains accessible from the local test machine
45+
using a host-specific connection string.
46+
4. Operations performed by the custom container on Azurite (e.g., creating
47+
a storage container) are visible and verifiable from the local machine.
48+
"""
49+
container_name = "test-container"
50+
with Network() as network:
51+
with (
52+
AzuriteContainer()
53+
.with_network(network)
54+
.with_network_aliases("azurite_server")
55+
.with_exposed_ports(10000, 10000)
56+
.with_exposed_ports(10001, 10001) as azurite_container
57+
):
58+
network_connection_string = azurite_container.get_connection_string(ConnectionStringType.NETWORK)
59+
local_connection_string = azurite_container.get_connection_string()
60+
with DockerImage(path=TEST_DIR / "samples/network_container", tag=IMAGE_TAG) as image:
61+
with (
62+
DockerContainer(image=str(image))
63+
.with_env("AZURE_CONNECTION_STRING", network_connection_string)
64+
.with_env("AZURE_CONTAINER", container_name)
65+
.with_network(network)
66+
.with_network_aliases("network_container")
67+
.with_exposed_ports(80, 80) as container
68+
):
69+
wait_for_logs(container, "Azure Storage Container created.")
70+
blob_service_client = BlobServiceClient.from_connection_string(
71+
local_connection_string, api_version="2019-12-12"
72+
)
73+
# make sure the container was actually created
74+
assert container_name in [
75+
blob_container["name"] for blob_container in blob_service_client.list_containers()
76+
]

0 commit comments

Comments
 (0)