Skip to content

Commit b98c304

Browse files
Merge branch 'main' into patch-2
2 parents e1ce158 + b21e5e3 commit b98c304

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)