Skip to content

Commit 0f554fb

Browse files
authored
fix(new): add a new Docker Registry test container (#389)
I added a new test container for spinning up a [Docker registry](https://hub.docker.com/_/registry).
1 parent 451d278 commit 0f554fb

File tree

6 files changed

+158
-1
lines changed

6 files changed

+158
-1
lines changed

index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ testcontainers-python facilitates the use of Docker containers for functional an
4040
modules/qdrant/README
4141
modules/rabbitmq/README
4242
modules/redis/README
43+
modules/registry/README
4344
modules/selenium/README
4445
modules/weaviate/README
4546

modules/registry/README.rst

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. autoclass:: testcontainers.registry.DockerRegistryContainer
2+
3+
When building Docker containers with Docker Buildx there is currently no option to test your containers locally without
4+
a local registry. Otherwise Buildx pushes your image to Docker Hub, which is not what you want in a test case. More
5+
and more you need to use Buildx for efficiently building images and especially multi arch images.
6+
7+
When you use Docker Python libraries like docker-py or python-on-whales to build and test Docker images, what a lot of
8+
persons and DevOps engineers like me nowadays do, a test container comes in very handy.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import time
2+
from io import BytesIO
3+
from tarfile import TarFile, TarInfo
4+
from typing import TYPE_CHECKING, Optional
5+
6+
import bcrypt
7+
from requests import get
8+
from requests.auth import HTTPBasicAuth
9+
from requests.exceptions import ConnectionError, ReadTimeout
10+
11+
from testcontainers.core.container import DockerContainer
12+
from testcontainers.core.waiting_utils import wait_container_is_ready
13+
14+
if TYPE_CHECKING:
15+
from requests import Response
16+
17+
18+
class DockerRegistryContainer(DockerContainer):
19+
# https://docs.docker.com/registry/
20+
credentials_path: str = "/htpasswd/credentials.txt"
21+
22+
def __init__(
23+
self,
24+
image: str = "registry:2",
25+
port: int = 5000,
26+
username: Optional[str] = None,
27+
password: Optional[str] = None,
28+
**kwargs,
29+
) -> None:
30+
super().__init__(image=image, **kwargs)
31+
self.port: int = port
32+
self.username: Optional[str] = username
33+
self.password: Optional[str] = password
34+
self.with_exposed_ports(self.port)
35+
36+
def _copy_credentials(self) -> None:
37+
# Create credentials and write them to the container
38+
hashed_password: str = bcrypt.hashpw(
39+
self.password.encode("utf-8"),
40+
bcrypt.gensalt(rounds=12, prefix=b"2a"),
41+
).decode("utf-8")
42+
content: bytes = f"{self.username}:{hashed_password}".encode("utf-8") # noqa: UP012
43+
44+
with BytesIO() as tar_archive_object, TarFile(fileobj=tar_archive_object, mode="w") as tmp_tarfile:
45+
tarinfo: TarInfo = TarInfo(name=self.credentials_path)
46+
tarinfo.size = len(content)
47+
tarinfo.mtime = time.time()
48+
49+
tmp_tarfile.addfile(tarinfo, BytesIO(content))
50+
tar_archive_object.seek(0)
51+
self.get_wrapped_container().put_archive("/", tar_archive_object)
52+
53+
@wait_container_is_ready(ConnectionError, ReadTimeout)
54+
def _readiness_probe(self) -> None:
55+
url: str = f"http://{self.get_registry()}/v2"
56+
if self.username and self.password:
57+
response: Response = get(url, auth=HTTPBasicAuth(self.username, self.password), timeout=1)
58+
else:
59+
response: Response = get(url, timeout=1)
60+
response.raise_for_status()
61+
62+
def start(self):
63+
if self.username and self.password:
64+
self.with_env("REGISTRY_AUTH_HTPASSWD_REALM", "local-registry")
65+
self.with_env("REGISTRY_AUTH_HTPASSWD_PATH", self.credentials_path)
66+
super().start()
67+
self._copy_credentials()
68+
else:
69+
super().start()
70+
71+
self._readiness_probe()
72+
return self
73+
74+
def get_registry(self) -> str:
75+
host: str = self.get_container_host_ip()
76+
port: str = self.get_exposed_port(self.port)
77+
return f"{host}:{port}"
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from requests import Response, get
2+
from requests.auth import HTTPBasicAuth
3+
from testcontainers.registry import DockerRegistryContainer
4+
5+
6+
REGISTRY_USERNAME: str = "foo"
7+
REGISTRY_PASSWORD: str = "bar"
8+
9+
10+
def test_registry():
11+
with DockerRegistryContainer().with_bind_ports(5000, 5000) as registry_container:
12+
url: str = f"http://{registry_container.get_registry()}/v2/_catalog"
13+
14+
response: Response = get(url)
15+
16+
assert response.status_code == 200
17+
18+
19+
def test_registry_with_authentication():
20+
with DockerRegistryContainer(username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD).with_bind_ports(
21+
5000, 5000
22+
) as registry_container:
23+
url: str = f"http://{registry_container.get_registry()}/v2/_catalog"
24+
25+
response: Response = get(url, auth=HTTPBasicAuth(REGISTRY_USERNAME, REGISTRY_PASSWORD))
26+
27+
assert response.status_code == 200

poetry.lock

+42-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ packages = [
5353
{ include = "testcontainers", from = "modules/qdrant" },
5454
{ include = "testcontainers", from = "modules/rabbitmq" },
5555
{ include = "testcontainers", from = "modules/redis" },
56+
{ include = "testcontainers", from = "modules/registry" },
5657
{ include = "testcontainers", from = "modules/selenium" },
5758
{ include = "testcontainers", from = "modules/weaviate" }
5859
]
@@ -94,6 +95,7 @@ selenium = { version = "*", optional = true }
9495
weaviate-client = { version = "^4.5.4", optional = true }
9596
chromadb-client = { version = "*", optional = true }
9697
qdrant-client = { version = "*", optional = true }
98+
bcrypt = { version = "*", optional = true }
9799

98100
[tool.poetry.extras]
99101
arangodb = ["python-arango"]
@@ -120,6 +122,7 @@ postgres = []
120122
qdrant = ["qdrant-client"]
121123
rabbitmq = ["pika"]
122124
redis = ["redis"]
125+
registry = ["bcrypt"]
123126
selenium = ["selenium"]
124127
weaviate = ["weaviate-client"]
125128
chroma = ["chromadb-client"]

0 commit comments

Comments
 (0)