Skip to content

Commit 8c28a86

Browse files
g0dialexanderankin
andauthored
feat(compose): add ability to get docker compose config (#669)
This PR add a new function to the `testcontainers.compose.DockerComposer` class, `get_config` which use `docker compose config` command for resolving and returning the actual docker compose configuration. This can be useful for example if you want to retrieve a connection string you pass to your app in your docker compose in order to connect to your database service instead of copy pasting it from your compose file into your tests. Also note thats its way easier to rely on docker compose config to get you the config than trying to manually find, read and merge compose files in specified context (I tried it first ...). About the tests I mostly ensured the docker compose command was as expected. This is because the config produced by the docker compose can not always reflect exactly what is in the file. There is some normalization/resolving which is done (even when you pass all flags to disable them). But anyway, I'm not sure its a good idea to actually test the behavior of the docker config command itself. Let me know what you think of it! --------- Co-authored-by: David Ankin <[email protected]>
1 parent e1e3d13 commit 8c28a86

File tree

4 files changed

+93
-2
lines changed

4 files changed

+93
-2
lines changed

core/testcontainers/compose/compose.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
from dataclasses import asdict, dataclass, field, fields
22
from functools import cached_property
33
from json import loads
4+
from logging import warning
45
from os import PathLike
56
from platform import system
67
from re import split
78
from subprocess import CompletedProcess
89
from subprocess import run as subprocess_run
9-
from typing import Callable, Literal, Optional, TypeVar, Union
10+
from typing import Any, Callable, Literal, Optional, TypeVar, Union, cast
1011
from urllib.error import HTTPError, URLError
1112
from urllib.request import urlopen
1213

1314
from testcontainers.core.exceptions import ContainerIsNotRunning, NoSuchPortExposed
1415
from testcontainers.core.waiting_utils import wait_container_is_ready
1516

1617
_IPT = TypeVar("_IPT")
18+
_WARNINGS = {"DOCKER_COMPOSE_GET_CONFIG": "get_config is experimental, see testcontainers/testcontainers-python#669"}
1719

1820

1921
def _ignore_properties(cls: type[_IPT], dict_: any) -> _IPT:
@@ -258,6 +260,36 @@ def get_logs(self, *services: str) -> tuple[str, str]:
258260
result = self._run_command(cmd=logs_cmd)
259261
return result.stdout.decode("utf-8"), result.stderr.decode("utf-8")
260262

263+
def get_config(
264+
self, *, path_resolution: bool = True, normalize: bool = True, interpolate: bool = True
265+
) -> dict[str, Any]:
266+
"""
267+
Parse, resolve and returns compose file via `docker config --format json`.
268+
In case of multiple compose files, the returned value will be a merge of all files.
269+
270+
See: https://docs.docker.com/reference/cli/docker/compose/config/ for more details
271+
272+
:param path_resolution: whether to resolve file paths
273+
:param normalize: whether to normalize compose model
274+
:param interpolate: whether to interpolate environment variables
275+
276+
Returns:
277+
Compose file
278+
279+
"""
280+
if "DOCKER_COMPOSE_GET_CONFIG" in _WARNINGS:
281+
warning(_WARNINGS.pop("DOCKER_COMPOSE_GET_CONFIG"))
282+
config_cmd = [*self.compose_command_property, "config", "--format", "json"]
283+
if not path_resolution:
284+
config_cmd.append("--no-path-resolution")
285+
if not normalize:
286+
config_cmd.append("--no-normalize")
287+
if not interpolate:
288+
config_cmd.append("--no-interpolate")
289+
290+
cmd_output = self._run_command(cmd=config_cmd).stdout
291+
return cast(dict[str, Any], loads(cmd_output))
292+
261293
def get_containers(self, include_all=False) -> list[ComposeContainer]:
262294
"""
263295
Fetch information about running containers via `docker compose ps --format json`.

core/tests/test_compose.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from urllib.request import urlopen, Request
77

88
import pytest
9+
from pytest_mock import MockerFixture
910

1011
from testcontainers.compose import DockerCompose, ContainerIsNotRunning, NoSuchPortExposed
1112

@@ -304,6 +305,45 @@ def test_exec_in_container_multiple():
304305
assert "test_exec_in_container" in body
305306

306307

308+
CONTEXT_FIXTURES = [pytest.param(ctx, id=ctx.name) for ctx in FIXTURES.iterdir()]
309+
310+
311+
@pytest.mark.parametrize("context", CONTEXT_FIXTURES)
312+
def test_compose_config(context: Path, mocker: MockerFixture) -> None:
313+
compose = DockerCompose(context)
314+
run_command = mocker.spy(compose, "_run_command")
315+
expected_cmd = [*compose.compose_command_property, "config", "--format", "json"]
316+
317+
received_config = compose.get_config()
318+
319+
assert received_config
320+
assert isinstance(received_config, dict)
321+
assert "services" in received_config
322+
assert run_command.call_args.kwargs["cmd"] == expected_cmd
323+
324+
325+
@pytest.mark.parametrize("context", CONTEXT_FIXTURES)
326+
def test_compose_config_raw(context: Path, mocker: MockerFixture) -> None:
327+
compose = DockerCompose(context)
328+
run_command = mocker.spy(compose, "_run_command")
329+
expected_cmd = [
330+
*compose.compose_command_property,
331+
"config",
332+
"--format",
333+
"json",
334+
"--no-path-resolution",
335+
"--no-normalize",
336+
"--no-interpolate",
337+
]
338+
339+
received_config = compose.get_config(path_resolution=False, normalize=False, interpolate=False)
340+
341+
assert received_config
342+
assert isinstance(received_config, dict)
343+
assert "services" in received_config
344+
assert run_command.call_args.kwargs["cmd"] == expected_cmd
345+
346+
307347
def fetch(req: Union[Request, str]):
308348
if isinstance(req, str):
309349
req = Request(method="GET", url=req)

poetry.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ paho-mqtt = "2.1.0"
182182
sqlalchemy-cockroachdb = "2.0.2"
183183
paramiko = "^3.4.0"
184184
types-paramiko = "^3.4.0.20240423"
185+
pytest-mock = "^3.14.0"
185186

186187
[[tool.poetry.source]]
187188
name = "PyPI"

0 commit comments

Comments
 (0)