Skip to content

Commit 27601db

Browse files
committed
Added a new Docker registry test container and tests
1 parent 6668ca4 commit 27601db

File tree

7 files changed

+123
-0
lines changed

7 files changed

+123
-0
lines changed

.github/workflows/main.yml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
- rabbitmq
4444
- redis
4545
- selenium
46+
- registry
4647
runs-on: ${{ matrix.runtime.machine }}
4748
steps:
4849
- uses: actions/checkout@v3

README.rst

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ testcontainers-python facilitates the use of Docker containers for functional an
3535
rabbitmq/README
3636
redis/README
3737
selenium/README
38+
registry/README
3839

3940
Getting Started
4041
---------------

registry/README.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.. autoclass:: testcontainers.registry.DockerRegistryContainer
2+

registry/setup.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from setuptools import setup, find_namespace_packages
2+
3+
description = "Docker registry component of testcontainers-python."
4+
5+
setup(
6+
name="testcontainers-registry",
7+
version="0.0.1rc1",
8+
packages=find_namespace_packages(),
9+
description=description,
10+
long_description=description,
11+
long_description_content_type="text/x-rst",
12+
url="https://github.com/testcontainers/testcontainers-python",
13+
install_requires=[
14+
"testcontainers-core",
15+
"bcrypt",
16+
],
17+
python_requires=">=3.7",
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import time
2+
from io import BytesIO
3+
from tarfile import TarFile, TarInfo
4+
from typing import Optional
5+
6+
import bcrypt
7+
from requests import Response, get
8+
from requests.auth import HTTPBasicAuth
9+
from requests.exceptions import ConnectionError, ReadTimeout
10+
from testcontainers.core.container import DockerContainer
11+
from testcontainers.core.waiting_utils import wait_container_is_ready
12+
13+
class DockerRegistryContainer(DockerContainer):
14+
# https://docs.docker.com/registry/
15+
credentials_path: str = "/htpasswd/credentials.txt"
16+
17+
def __init__(
18+
self,
19+
image: str = "registry:2",
20+
port: int = 5000,
21+
username: str = None,
22+
password: str = None,
23+
**kwargs,
24+
) -> None:
25+
super().__init__(image=image, **kwargs)
26+
self.port: int = port
27+
self.username: Optional[str] = username
28+
self.password: Optional[str] = password
29+
self.with_exposed_ports(self.port)
30+
31+
def _copy_credentials(self) -> None:
32+
# Create credentials and write them to the container
33+
hashed_password: str = bcrypt.hashpw(
34+
self.password.encode("utf-8"),
35+
bcrypt.gensalt(rounds=12, prefix=b"2a"),
36+
).decode("utf-8")
37+
content = f"{self.username}:{hashed_password}".encode("utf-8")
38+
39+
with BytesIO() as tar_archive_object, TarFile(
40+
fileobj=tar_archive_object, mode="w"
41+
) as tmp_tarfile:
42+
tarinfo: TarInfo = TarInfo(name=self.credentials_path)
43+
tarinfo.size = len(content)
44+
tarinfo.mtime = time.time()
45+
46+
tmp_tarfile.addfile(tarinfo, BytesIO(content))
47+
tar_archive_object.seek(0)
48+
self.get_wrapped_container().put_archive("/", tar_archive_object)
49+
50+
@wait_container_is_ready(ConnectionError, ReadTimeout)
51+
def _readiness_probe(self) -> None:
52+
url: str = f"http://{self.get_registry()}/v2"
53+
if self.username and self.password:
54+
response: Response = get(url, auth=HTTPBasicAuth(self.username, self.password), timeout=1)
55+
else:
56+
response: Response = get(url, timeout=1)
57+
response.raise_for_status()
58+
59+
def start(self):
60+
if self.username and self.password:
61+
self.with_env("REGISTRY_AUTH_HTPASSWD_REALM", "local-registry")
62+
self.with_env("REGISTRY_AUTH_HTPASSWD_PATH", self.credentials_path)
63+
super().start()
64+
self._copy_credentials()
65+
else:
66+
super().start()
67+
68+
self._readiness_probe()
69+
return self
70+
71+
def get_registry(self) -> str:
72+
host: str = self.get_container_host_ip()
73+
port: str = self.get_exposed_port(self.port)
74+
return f"{host}:{port}"

registry/tests/test_registry.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
def test_registry():
10+
with DockerRegistryContainer().with_bind_ports(5000, 5000) as registry_container:
11+
url: str = f"http://{registry_container.get_registry()}/v2/_catalog"
12+
13+
response: Response = get(url)
14+
15+
assert response.status_code == 200
16+
17+
18+
def test_registry_with_authentication():
19+
with DockerRegistryContainer(
20+
username=REGISTRY_USERNAME, password=REGISTRY_PASSWORD
21+
).with_bind_ports(5000, 5000) as registry_container:
22+
url: str = f"http://{registry_container.get_registry()}/v2/_catalog"
23+
24+
response: Response = get(url, auth=HTTPBasicAuth(REGISTRY_USERNAME, REGISTRY_PASSWORD))
25+
26+
assert response.status_code == 200

requirements.in

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
-e file:rabbitmq
2121
-e file:redis
2222
-e file:selenium
23+
-e file:registry
2324
cryptography<37
2425
flake8<3.8.0 # 3.8.0 adds a dependency on importlib-metadata which conflicts with other packages.
2526
pg8000

0 commit comments

Comments
 (0)