Skip to content

Commit b21e5e3

Browse files
authored
feat(azurite): Enhance connection string generation for network and local access (#859)
**Resolves #851** This Pull Request enhances the `AzuriteContainer` to provide more flexible and robust connection strings, specifically enabling seamless communication between Azurite and other containers within the same Docker network. It also clarifies access from the local host, addressing the need for distinct connection types in containerized testing environments. --- ### Key Changes * **Introduces `ConnectionStringType` enum:** This new enum allows specifying the intended access pattern when requesting an Azurite connection string: * `NETWORK`: Optimized for inter-container communication, leveraging Docker network aliases for direct connectivity. * `LOCALHOST`: Designed for access from the host machine, utilizing dynamically exposed ports and the Docker host IP. * **Refactored `get_connection_string`:** The main method now dispatches to the appropriate internal function based on the `ConnectionStringType` provided. * **Improved `get_external_connection_string`:** This method has been enhanced to intelligently prioritize network aliases for inter-container communication and provide a robust fallback to the Docker host IP for other scenarios. * **Comprehensive Unit Test:** A new test case, `test_azurite_inter_container_communication_with_network_string`, has been added to thoroughly validate both network-based and local host-based connection string functionality. * **Enhanced Docstrings:** All relevant methods within `AzuriteContainer` and the associated example application (`network_container.py`) have received updated and more detailed docstrings for improved clarity and usage guidance. --- ### Testing To validate these functionalities, the new test case `test_azurite_inter_container_communication_with_network_string` was introduced. This test specifically verifies: * **Inter-container communication:** An external application container successfully connects to Azurite using the `NETWORK` connection string (via its network alias) and performs an operation (e.g., creates a blob container). * **Local host access verification:** The operation performed by the external container is then successfully verified from the local test machine using the `LOCALHOST` connection string, confirming data persistence and accessibility. --- ### Concerns and Questions As this is my first contribution to this repository, I've aimed for comprehensive docstrings and clear code. I'm open to feedback on their level of detail, adherence to project conventions, or any other aspect of the implementation. Please let me know if any further changes or clarifications are needed.
1 parent fc4155e commit b21e5e3

File tree

4 files changed

+227
-2
lines changed

4 files changed

+227
-2
lines changed

modules/azurite/testcontainers/azurite/__init__.py

Lines changed: 123 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,45 @@ 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+
if connection_string_type == ConnectionStringType.LOCALHOST:
110+
return self.__get_local_connection_string()
111+
elif connection_string_type == ConnectionStringType.NETWORK:
112+
return self.__get_external_connection_string()
113+
else:
114+
raise ValueError(
115+
f"unrecognized connection string type {connection_string_type}, "
116+
f"Supported values are ConnectionStringType.LOCALHOST or ConnectionStringType.NETWORK "
117+
)
118+
119+
def __get_local_connection_string(self) -> str:
120+
"""Generates a connection string for Azurite accessible from the local host machine.
121+
122+
This connection string uses the Docker host IP address (obtained via
123+
:meth:`testcontainers.core.container.DockerContainer.get_container_host_ip`)
124+
and the dynamically exposed ports of the Azurite container. This ensures that
125+
clients running on the host can connect successfully to the Azurite services.
126+
127+
:return: The Azurite connection string for local host access.
128+
:rtype: str
129+
"""
77130
host_ip = self.get_container_host_ip()
78131
connection_string = (
79132
f"DefaultEndpointsProtocol=http;AccountName={self.account_name};AccountKey={self.account_key};"
@@ -96,6 +149,75 @@ def get_connection_string(self) -> str:
96149

97150
return connection_string
98151

152+
def __get_external_connection_string(self) -> str:
153+
"""Generates a connection string for Azurite, primarily optimized for
154+
inter-container communication within a custom Docker network.
155+
156+
This method attempts to provide the most suitable connection string
157+
based on the container's network configuration:
158+
159+
- **For Inter-Container Communication (Recommended):** If the Azurite container is
160+
part of a custom Docker network and has network aliases configured,
161+
the connection string will use the first network alias as the hostname
162+
and the internal container ports (e.g., #$#`http://<alias>:<internal_port>/<account_name>`#$#).
163+
This is the most efficient and robust way for other containers
164+
in the same network to connect to Azurite, leveraging Docker's internal DNS.
165+
166+
- **Fallback for Non-Networked/Aliased Scenarios:** If the container is
167+
not on a custom network with aliases (e.g., running on the default
168+
bridge network without explicit aliases), the method falls back to
169+
using the Docker host IP (obtained via
170+
:meth:`testcontainers.core.container.DockerContainer.get_container_host_ip`)
171+
and the dynamically exposed ports (e.g., #$#`http://<host_ip>:<exposed_port>/<account_name>`#$#).
172+
While this connection string is technically "external" to the container,
173+
it primarily facilitates connections *from the host machine*.
174+
175+
:return: The generated Azurite connection string.
176+
:rtype: str
177+
"""
178+
# Check if we're on a custom network and have network aliases
179+
if hasattr(self, "_network") and self._network and hasattr(self, "_network_aliases") and self._network_aliases:
180+
# Use the first network alias for inter-container communication
181+
host_ip = self._network_aliases[0]
182+
# When using network aliases, use the internal container ports
183+
blob_port = self.blob_service_port
184+
queue_port = self.queue_service_port
185+
table_port = self.table_service_port
186+
else:
187+
# Use the Docker host IP for external connections
188+
host_ip = self.get_container_host_ip()
189+
# When using host IP, use the exposed ports
190+
blob_port = (
191+
self.get_exposed_port(self.blob_service_port)
192+
if self.blob_service_port in self.ports
193+
else self.blob_service_port
194+
)
195+
queue_port = (
196+
self.get_exposed_port(self.queue_service_port)
197+
if self.queue_service_port in self.ports
198+
else self.queue_service_port
199+
)
200+
table_port = (
201+
self.get_exposed_port(self.table_service_port)
202+
if self.table_service_port in self.ports
203+
else self.table_service_port
204+
)
205+
206+
connection_string = (
207+
f"DefaultEndpointsProtocol=http;AccountName={self.account_name};AccountKey={self.account_key};"
208+
)
209+
210+
if self.blob_service_port in self.ports:
211+
connection_string += f"BlobEndpoint=http://{host_ip}:{blob_port}/{self.account_name};"
212+
213+
if self.queue_service_port in self.ports:
214+
connection_string += f"QueueEndpoint=http://{host_ip}:{queue_port}/{self.account_name};"
215+
216+
if self.table_service_port in self.ports:
217+
connection_string += f"TableEndpoint=http://{host_ip}:{table_port}/{self.account_name};"
218+
219+
return connection_string
220+
99221
def start(self) -> "AzuriteContainer":
100222
super().start()
101223
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)