Skip to content

Commit 1195a4b

Browse files
authored
Introduce docker_fixtures (#5610)
1 parent b81f8b7 commit 1195a4b

File tree

8 files changed

+874
-727
lines changed

8 files changed

+874
-727
lines changed

tests/parametric/conftest.py

Lines changed: 16 additions & 710 deletions
Large diffs are not rendered by default.

tests/parametric/test_otel_logs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ def find_attributes(proto_object):
5353

5454

5555
@pytest.fixture
56-
def otlp_endpoint_library_env(library_env, endpoint_env, test_agent_container_name, test_agent_otlp_grpc_port):
56+
def otlp_endpoint_library_env(library_env, endpoint_env, test_agent, test_agent_otlp_grpc_port):
5757
"""Set up a custom endpoint for OTLP logs."""
5858
prev_value = library_env.get(endpoint_env)
59-
library_env[endpoint_env] = f"http://{test_agent_container_name}:{test_agent_otlp_grpc_port}"
59+
library_env[endpoint_env] = f"http://{test_agent.container_name}:{test_agent_otlp_grpc_port}"
6060
yield library_env
6161
if prev_value is None:
6262
del library_env[endpoint_env]

tests/parametric/test_otel_metrics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545

4646
@pytest.fixture
4747
def otlp_metrics_endpoint_library_env(
48-
library_env, endpoint_env, test_agent_otlp_http_port, test_agent_otlp_grpc_port, test_agent_container_name
48+
library_env, endpoint_env, test_agent, test_agent_otlp_http_port, test_agent_otlp_grpc_port
4949
):
5050
"""Set up a custom endpoint for OTLP metrics."""
5151
prev_value = library_env.get(endpoint_env)
@@ -59,7 +59,7 @@ def otlp_metrics_endpoint_library_env(
5959
port = test_agent_otlp_grpc_port if protocol == "grpc" else test_agent_otlp_http_port
6060
path = "/" if protocol == "grpc" or endpoint_env == "OTEL_EXPORTER_OTLP_ENDPOINT" else "/v1/metrics"
6161

62-
library_env[endpoint_env] = f"http://{test_agent_container_name}:{port}{path}"
62+
library_env[endpoint_env] = f"http://{test_agent.container_name}:{port}{path}"
6363
yield library_env
6464
if prev_value is None:
6565
del library_env[endpoint_env]

utils/_context/_scenarios/parametric.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,11 @@
1313
import pytest
1414
from _pytest.outcomes import Failed
1515
from docker.models.containers import Container
16-
from docker.models.networks import Network
17-
from retry import retry
1816

17+
from utils.docker_fixtures._test_agent import TestAgentFactory, TestAgentAPI
1918
from utils._context.component_version import ComponentVersion
20-
from utils._logger import logger
21-
2219
from utils._context.docker import get_docker_client
20+
from utils._logger import logger
2321
from .core import Scenario, scenario_groups
2422

2523

@@ -56,9 +54,10 @@ class APMLibraryTestServer:
5654

5755

5856
class ParametricScenario(Scenario):
59-
TEST_AGENT_IMAGE = "ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:v1.32.0"
6057
apm_test_server_definition: APMLibraryTestServer
6158

59+
_test_agent_factory: TestAgentFactory
60+
6261
class PersistentParametricTestConf(dict):
6362
"""Parametric tests are executed in multiple thread, we need a mechanism to persist
6463
each parametrized_tests_metadata on a file
@@ -123,13 +122,14 @@ def configure(self, config: pytest.Config):
123122
"rust": rust_library_factory,
124123
}[library]
125124

125+
self._test_agent_factory = TestAgentFactory(self.host_log_folder)
126126
self.apm_test_server_definition = factory()
127127

128128
if self.is_main_worker:
129129
# https://github.com/pytest-dev/pytest-xdist/issues/271#issuecomment-826396320
130130
# we are in the main worker, not in a xdist sub-worker
131131
self._build_apm_test_server_image(config.option.github_token_file)
132-
self._pull_test_agent_image()
132+
self._test_agent_factory.pull()
133133
self._clean_containers()
134134
self._clean_networks()
135135

@@ -161,11 +161,6 @@ def get_warmups(self):
161161

162162
return result
163163

164-
@retry(delay=10, tries=3)
165-
def _pull_test_agent_image(self):
166-
logger.stdout("Pulling test agent image...")
167-
get_docker_client().images.pull(self.TEST_AGENT_IMAGE)
168-
169164
def _clean_containers(self):
170165
"""Some containers may still exists from previous unfinished sessions"""
171166

@@ -258,10 +253,21 @@ def _build_apm_test_server_image(self, github_token_file: str) -> None:
258253

259254
logger.debug("Build tested container finished")
260255

261-
def create_docker_network(self, test_id: str) -> Network:
256+
@contextlib.contextmanager
257+
def _get_docker_network(self, test_id: str) -> Generator[str, None, None]:
262258
docker_network_name = f"{_NETWORK_PREFIX}_{test_id}"
259+
network = get_docker_client().networks.create(name=docker_network_name, driver="bridge")
263260

264-
return get_docker_client().networks.create(name=docker_network_name, driver="bridge")
261+
try:
262+
yield network.name
263+
finally:
264+
try:
265+
network.remove()
266+
except:
267+
# It's possible (why?) of having some container not stopped.
268+
# If it happens, failing here makes stdout tough to understand.
269+
# Let's ignore this, later calls will clean the mess
270+
logger.info("Failed to remove network, ignoring the error")
265271

266272
@staticmethod
267273
def get_host_port(worker_id: str, base_port: int) -> int:
@@ -341,6 +347,28 @@ def get_junit_properties(self) -> dict[str, str]:
341347

342348
return result
343349

350+
@contextlib.contextmanager
351+
def get_test_agent_api(
352+
self,
353+
worker_id: str,
354+
request: pytest.FixtureRequest,
355+
test_id: str,
356+
container_otlp_http_port: int,
357+
container_otlp_grpc_port: int,
358+
) -> Generator[TestAgentAPI, None, None]:
359+
with (
360+
self._get_docker_network(test_id) as docker_network,
361+
self._test_agent_factory.get_test_agent_api(
362+
request=request,
363+
worker_id=worker_id,
364+
docker_network=docker_network,
365+
container_name=f"ddapm-test-agent-{test_id}",
366+
container_otlp_http_port=container_otlp_http_port,
367+
container_otlp_grpc_port=container_otlp_grpc_port,
368+
) as result,
369+
):
370+
yield result
371+
344372

345373
def _get_base_directory() -> str:
346374
return str(Path.cwd())

utils/docker_fixtures/__init__.py

Whitespace-only changes.

utils/docker_fixtures/_core.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import contextlib
2+
from collections.abc import Generator
3+
from pathlib import Path
4+
from typing import TextIO
5+
6+
from docker.models.containers import Container
7+
import pytest
8+
9+
from utils._logger import logger
10+
from utils._context.docker import get_docker_client
11+
12+
13+
def get_host_port(worker_id: str, base_port: int) -> int:
14+
"""Deterministic port allocation for each worker"""
15+
16+
if worker_id == "master": # xdist disabled
17+
return base_port
18+
19+
if worker_id.startswith("gw"):
20+
return base_port + int(worker_id[2:])
21+
22+
raise ValueError(f"Unexpected worker_id: {worker_id}")
23+
24+
25+
def _compute_volumes(volumes: dict[str, str]) -> dict[str, dict]:
26+
"""Convert volumes to the format expected by the docker-py API"""
27+
fixed_volumes: dict[str, dict] = {}
28+
for key, value in volumes.items():
29+
# when host path starts with ./, resolve it from cwd()
30+
fixed_key = str(Path.cwd().joinpath(key)) if key.startswith("./") else key
31+
32+
if isinstance(value, dict):
33+
fixed_volumes[fixed_key] = value
34+
elif isinstance(value, str):
35+
fixed_volumes[fixed_key] = {"bind": value, "mode": "rw"}
36+
else:
37+
raise TypeError(f"Unexpected type for volume {key}: {type(value)}")
38+
39+
return fixed_volumes
40+
41+
42+
@contextlib.contextmanager
43+
def docker_run(
44+
image: str,
45+
name: str,
46+
env: dict[str, str],
47+
volumes: dict[str, str],
48+
network: str,
49+
ports: dict[str, int],
50+
log_file: TextIO,
51+
command: list[str] | None = None,
52+
) -> Generator[Container, None, None]:
53+
logger.info(f"Run container {name} from image {image} with ports {ports}")
54+
55+
try:
56+
container: Container = get_docker_client().containers.run(
57+
image,
58+
name=name,
59+
environment=env,
60+
volumes=_compute_volumes(volumes),
61+
network=network,
62+
ports=ports,
63+
command=command,
64+
detach=True,
65+
)
66+
logger.debug(f"Container {name} successfully started")
67+
except Exception as e:
68+
# at this point, even if it failed to start, the container may exists!
69+
for container in get_docker_client().containers.list(filters={"name": name}, all=True):
70+
container.remove(force=True)
71+
72+
pytest.fail(f"Failed to run container {name}: {e}")
73+
74+
try:
75+
yield container
76+
finally:
77+
logger.info(f"Stopping {name}")
78+
container.stop(timeout=1)
79+
logs = container.logs()
80+
log_file.write(logs.decode("utf-8"))
81+
log_file.flush()
82+
container.remove(force=True)

0 commit comments

Comments
 (0)