Skip to content

Commit aa23ee0

Browse files
vitkyrkaclaude
andauthored
Add discovery config spec tooling and krakend reference implementation (#24126)
* Remove __discovery_provides__ attribute and dead-code tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add discovery config spec validation and model generation to ddev. Adds a discovery section to the spec schema (port_hints, strategy field), extends the example and model consumers to render/generate discovery config, adds spec.py validation for discovery spec correctness, and covers the new paths with tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * add annotations import * Add krakend discovery config support with e2e discovery test helpers. Adds krakend config_models/discovery.py and auto_conf.yaml for port-based config autodiscovery. Adds get_e2e_discovery_metadata() to ddev docker helpers and a dd_agent_check_discovery pytest fixture so integrations can run discovery-path E2E tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * regen * add test to force probe of all ports/configs to check no side effects on server To ensure that this no risk with probing the ports of the container for configuration discovery, add a test which instanciates the integration which each of the generated configurations in turn and checks that the container logs don't have any obvious crashes. https://datadoghq.atlassian.net/wiki/spaces/DSCVR/pages/6671862234/Configuration+Discovery+for+Agent+Integrations#Probing-causes-problems While the logic for detecting "bad" effects is best-effort, the test also the logs from this scenario to be seen easier during development. For example, for krakend: ``` $ ddev env test --base --new-env krakend py3.13-2.10 -- -k test_e2e_discovery_candidates_do_not_destabilize_container -rP --log-level=DEBUG ... _________ test_e2e_discovery_candidates_do_not_destabilize_container __________ ------------------------------ Captured log call ------------------------------- DEBUG root:docker.py:90 Probing candidate #1: {'init_config': {}, 'instances': [{'openmetrics_endpoint': 'http://172.17.129.3:9090/metrics'}]} DEBUG root:docker.py:90 Probing candidate #2: {'init_config': {}, 'instances': [{'openmetrics_endpoint': 'http://172.17.129.3:8080/metrics'}]} DEBUG root:docker.py:94 Error probing candidate #2: {'init_config': {}, 'instances': [{'openmetrics_endpoint': 'http://172.17.129.3:8080/metrics'}]} DEBUG root:docker.py:103 New log line: [GIN] 2026/06/17 - 15:31:46 | 404 | 2.803µs | 172.17.129.1 | GET "/metrics" DEBUG root:docker.py:103 New log line: [GIN] 2026/06/17 - 15:31:47 | 404 | 2.753µs | 172.17.129.1 | GET "/metrics" DEBUG root:docker.py:90 Probing candidate #3: {'init_config': {}, 'instances': [{'openmetrics_endpoint': 'http://172.17.129.3:8090/metrics'}]} DEBUG root:docker.py:94 Error probing candidate #3: {'init_config': {}, 'instances': [{'openmetrics_endpoint': 'http://172.17.129.3:8090/metrics'}]} DEBUG root:docker.py:103 New log line: [GIN] 2026/06/17 - 15:31:49 | 200 | 51.868µs | ::1 | GET "/__health" ``` * Spec-driven discovery redesign: tooling + krakend PoC Move from_ports from a runtime function in datadog_checks_base to a codegen strategy in datadog_checks_dev, and introduce a registry-driven model consumer that generates candidates() using Pydantic models. Key changes: - New discovery registry package with @strategy decorator and REGISTRY - core_strategies.py registers from_ports as a codegen strategy - _build_discovery_file is now registry-driven (no hardcoded from_ports) - Generated candidates() use InstanceConfig/SharedConfig model_dump (D1) - Remove ad_identifiers from spec discovery stanza (D2: hand-maintained) - Remove auto_conf.yaml generation from example consumer (D2) - Add discovery_strategies.py and discovery_overrides.py to CUSTOM_FILES - Regenerate krakend discovery.py with model-backed candidate output - Add hand-maintained krakend/auto_conf.yaml with ad_identifiers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Use model_validate with context for discovery candidate generation Direct InstanceConfig/SharedConfig construction fails because the field validators access info.context['configured_fields'], which is None without explicit context. Switch to model_validate with the required context dict so that all defaults from defaults.py are applied correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * T3/T4: extract expand_template_items, restructure discovery validation Extract expand_template_items from the template expansion loop in options_validator so it can be shared with discovery strategy lists. Add handle_discovery which runs after options_validator so discovery validation sees resolved options. Discovery now supports enabled:false kill-switch, local: strategy names, and candidate field cross-check against resolved instance options. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Refactor options_validator to call expand_template_items Remove the inline template expansion loop from options_validator and replace it with a call to expand_template_items, which was extracted in T3 precisely so both callers share one implementation. The only behavioral difference from the original was a per-item overrides dict; aligning expand_template_items to use shared overrides (accumulated across all items) preserves the existing semantics. hide_template was redundant: template.update(option) already carries any hidden:true from intermediate templates into the expanded item, so setdefault('hidden', False) produces the same result in all cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Make spec-driven discovery tooling fully registry-driven Align the discovery tooling with the implementation plan: a strategy now owns both its contract (provides/inputs) and its codegen (emit_context) in the dev-side registry, and the generator collects each strategy's runtime imports and emits the model-backed candidate body. No strategy name or input is special-cased in the generator or validator. - registry: add Input and Strategy(provides, inputs, runtime_imports, emit_context); add guarded SERVICE_FIELDS/PORT_FIELDS constants - core_strategies: from_ports emits only the candidate loop and ctx - model_consumer: registry-driven generator that builds candidates through the config models, supports local: strategies, and emits the discovery_strategies.py/discovery_overrides.py first-render stubs - spec: derive candidate placeholders from provides + the field constants, validate strategy inputs generically, honour enabled:false as a kill switch, resolve discovery-level templates - base: add a guard test asserting the dev field constants match the Service/Port pydantic models so the hand-copied fact cannot drift - fix the datadog_checks_dev discovery changelog entry PR number Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * krakend: regenerate discovery models and drop stray auto_conf.yaml - add the regenerated discovery_overrides.py custom-file stub - remove the misplaced package-root auto_conf.yaml; the Agent-facing opt-in file lives in data/auto_conf.yaml - point the discovery changelog entry at the correct PR number Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix discovery strategy validation * Wire discovery_overrides.py seam into generated discovery.py Generated discovery.py now delegates to the discovery_overrides.py custom file via the same pattern validators.py already uses: import the always- co-generated module and resolve candidates() through getattr with a fallback to the generated generator. Behaviorally identical while the stub is empty; live once an integration fills in candidates(service, default), with no further tooling PR. Regenerated krakend's discovery.py accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * krakend: regenerate discovery stub docs from current constants The discovery_strategies.py / discovery_overrides.py stubs are write-once CUSTOM_FILES, so they kept the wording from an earlier render and never re-synced with the updated documentation constants. Delete and regenerate them so the in-tree stubs match the current model_consumer constants. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * krakend: match changelog text to phase 2 spec Use the wording prescribed by the phase 2 plan (T9): "Add configuration discovery support." Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix template-expansion regressions in discovery config tooling Two issues surfaced by review of the discovery config tooling refactor: - expand_template_items dropped wrapper attributes (e.g. `hidden: true`) when a template resolved to a list, so the common `- template: instances/pdh_legacy` + `hidden: true` pattern used by aspdotnet, dotnetclr, hyperv, exchange_server, and active_directory stopped hiding the expanded options. Propagate the wrapper's leftover attributes to each expanded item via setdefault, restoring the pre-refactor behaviour. - The discovery model consumer emitted candidate templates inside a single-quoted literal, producing invalid discovery.py when a template contained a single quote or backslash. Use repr() to emit a properly escaped literal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add test for hidden propagation on list-template expansion Locks in the fix: a `hidden: true` wrapper on a list template applies to every expanded item, while an item's own explicit `hidden` value wins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Replace chr(10) workaround with a plain newline join chr(10) was the pre-3.12 workaround for using a backslash inside an f-string expression. Build the joined message first, matching the existing style in expand_template_items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Restore master template-expansion behavior in spec tooling The discovery-config refactor changed two aspects of option-template expansion in `expand_template_items`, both of which broke config/models validation for existing integrations: - Overrides were scoped per-item, so an override targeting a nested template (e.g. `extra_metrics.value.example` in the Windows perf-counter specs) was attempted prematurely on the parent template, reported as an error, and discarded before the nested template was spliced in and expanded. Restore the shared-override semantics: `apply_overrides` pops each override on success, so unresolved ones are retried against later items, and only truly-unused overrides are reported. - `hidden` propagation was scoped to a template's own expanded items, dropping the historical cross-sibling leak where a template wrapper's `hidden: true` carries onto following plain options until the next template. haproxy relies on this to hide its legacy (non-OpenMetrics) option block. Reintroduce it behind a `propagate_hidden` flag (on for options, off for discovery strategies, which have no `hidden` concept). Rendered example/model output is now byte-identical to master across all 263 integration specs. Adds a regression test for the nested-template override case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * remove test_discovery_stanza_does_not_emit_auto_conf * Stop reserving auto_conf.yaml name in discovery handling Discovery no longer generates auto_conf.yaml (it is hand-maintained per integration), so reserving the example name produced a false-positive collision for the legitimate "discovery stanza + hand-maintained auto_conf.yaml" combination. Drop the reservation, its plumbing through handle_discovery, and the test that pinned the false positive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Drop redundant hidden setdefault in options_validator `expand_template_items(propagate_hidden=True)` now sets `hidden` on every resolved option during expansion, so the `option.setdefault('hidden', False)` in options_validator was a dead no-op that misleadingly read as the source of the default. Remove it; rendered example/model output is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add discovery/openmetrics_from_ports and auto_conf/discovery spec templates Adds two reusable spec tooling templates: - discovery/openmetrics_from_ports: a from_ports strategy candidate that generates an openmetrics_endpoint URL from the discovered host and port. Integrations override port_hints to set the expected port. - auto_conf/discovery: the common options block for fleet-discovery auto_conf.yaml files (discovery: {}, init_config, instances: []). Each integration sets ad_identifiers independently and includes this template for the rest. Converts the krakend spec to use both templates and regenerates the krakend auto_conf.yaml and config_models/discovery.py from the spec. Also fixes ModelConsumer._process_section to return early for leaf options (no sub-options key) so that fleet-discovery auto_conf.yaml files, which use leaf instances: [] rather than a full instances section, do not crash validate models. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * modify auto_config * Narrow model consumer leaf-instances guard to instances section only The previous fix skipped any section without an `options` key before even checking whether it was init_config or instances. Tighten it to only apply inside the `instances` branch, where auto_conf.yaml files using the fleet-discovery pattern emit a leaf `instances: []` with no model schema. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Restrict leaf-instances guard to auto_conf.yaml files only Pass the spec file name into _process_section and apply the no-options early-return only when processing auto_conf.yaml, where a leaf instances: [] is valid for fleet discovery. Any other file with a leaf instances section still hits the normal code path and fails visibly. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Use by_alias=True when dumping discovery candidate configs Without this, options with hyphenated names (e.g. slowlog-max-len) are emitted as Python field names (slowlog_max_len) in the generated candidate dict. The Agent passes this dict verbatim as the raw instance config, so checks that look up the original hyphenated key would not find it. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Skip SharedConfig import/usage when spec has no init_config section When a spec declares discovery but omits init_config, no shared.py is generated. The discovery file unconditionally imported SharedConfig, causing ModuleNotFoundError at import time and silently preventing any candidates from being produced. Pass has_shared=False to _build_discovery_file in that case; the generated file emits `shared = {}` instead and omits the import. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * regen --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 554e8cb commit aa23ee0

25 files changed

Lines changed: 1912 additions & 81 deletions

File tree

datadog_checks_base/tests/base/utils/discovery/test_discovery.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,20 @@ def test_candidate_ports_prefers_hints_and_deduplicates():
209209
]
210210

211211

212+
def test_dev_placeholder_field_constants_match_models():
213+
"""Guard the one fact datadog_checks_dev must hand-copy from base.
214+
215+
The discovery tooling cannot import datadog_checks_base, so it keeps the
216+
Service/Port field names as constants used to validate candidate-template
217+
placeholders. This test fails if those constants drift from the real models.
218+
"""
219+
pytest.importorskip('datadog_checks.dev.tooling.configuration.discovery.registry')
220+
from datadog_checks.dev.tooling.configuration.discovery.registry import PORT_FIELDS, SERVICE_FIELDS
221+
222+
assert SERVICE_FIELDS == set(Service.model_fields)
223+
assert PORT_FIELDS == set(Port.model_fields)
224+
225+
212226
def test_discovery_strategy_passes_complete_contexts():
213227
from datadog_checks.base.utils.discovery import discovery_strategy
214228

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add tooling support for generating configuration discovery files.

datadog_checks_dev/datadog_checks/dev/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
from .__about__ import __version__
55
from .conditions import WaitFor
6-
from .docker import docker_run, get_docker_hostname
6+
from .docker import docker_run, get_docker_hostname, get_e2e_discovery_metadata
77
from .env import environment_run
88
from .errors import RetryError
99
from .fs import chdir, get_here, temp_chdir, temp_dir

datadog_checks_dev/datadog_checks/dev/docker.py

Lines changed: 222 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# (C) Datadog, Inc. 2018-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
import json
5+
import logging
46
import os
7+
import re
8+
from collections.abc import Callable, Mapping, Sequence
59
from contextlib import contextmanager
6-
from typing import Iterator # noqa: F401
10+
from types import SimpleNamespace
11+
from typing import Any, Iterator # noqa: F401
712
from urllib.parse import urlparse
813

914
from .conditions import CheckDockerLogs
@@ -20,6 +25,16 @@
2025
from contextlib2 import ExitStack
2126

2227

28+
CONTAINER_STABILITY_LOG_PATTERNS = (
29+
r'error',
30+
r'panic',
31+
r'fatal',
32+
r'segmentation fault',
33+
r'core dumped',
34+
r'Traceback',
35+
)
36+
37+
2338
def get_docker_hostname():
2439
"""
2540
Determine the hostname Docker uses based on the environment, defaulting to `localhost`.
@@ -42,6 +57,202 @@ def get_container_ip(container_id_or_name):
4257
return run_command(command, capture='out', check=True).stdout.strip()
4358

4459

60+
def assert_all_discovery_candidates_stable(
61+
dd_agent_check: Callable[..., Any],
62+
check_cls: type[Any],
63+
compose_file: str | os.PathLike[str] | None = None,
64+
compose_service: str | None = None,
65+
*,
66+
project_name: str | None = None,
67+
service_id: str | None = None,
68+
dd_agent_check_kwargs: Mapping[str, Any] | None = None,
69+
log_patterns: Sequence[str] = CONTAINER_STABILITY_LOG_PATTERNS,
70+
) -> None:
71+
"""Run generated discovery candidates directly and assert the target container stays stable."""
72+
compose_service = compose_service or _get_default_compose_service()
73+
container_id = _get_compose_container_id(compose_file, compose_service, project_name=project_name)
74+
initial_state = _inspect_container(container_id)
75+
previous_logs = _get_container_logs(container_id)
76+
77+
ports = tuple(SimpleNamespace(number=port, name='') for port in _get_container_ports(initial_state))
78+
service = SimpleNamespace(
79+
id=service_id or compose_service,
80+
host=_get_container_ip_from_inspect(initial_state),
81+
ports=ports,
82+
)
83+
candidates = tuple(check_cls.generate_configs(service))
84+
if not candidates:
85+
raise AssertionError(f'No discovery candidates generated for service {service.id!r}')
86+
87+
check_kwargs = {'check_rate': True}
88+
if dd_agent_check_kwargs:
89+
check_kwargs.update(dd_agent_check_kwargs)
90+
91+
for index, candidate in enumerate(candidates, 1):
92+
logging.debug('Probing candidate #%d: %r', index, candidate)
93+
try:
94+
dd_agent_check(candidate, **check_kwargs)
95+
except Exception:
96+
logging.debug('Error probing candidate #%d: %r', index, candidate)
97+
pass
98+
99+
current_container_id = _get_compose_container_id(compose_file, compose_service, project_name=project_name)
100+
current_state = _inspect_container(current_container_id)
101+
_assert_container_stable(initial_state, current_state, index)
102+
103+
current_logs = _get_container_logs(current_container_id)
104+
new_logs = current_logs[len(previous_logs) :] if current_logs.startswith(previous_logs) else current_logs
105+
for line in new_logs.splitlines():
106+
logging.debug('New log line: %s', line)
107+
_assert_no_log_patterns(new_logs, log_patterns, index)
108+
previous_logs = current_logs
109+
110+
111+
def _get_compose_container_id(
112+
compose_file: str | os.PathLike[str] | None, compose_service: str, *, project_name: str | None = None
113+
) -> str:
114+
compose_file = compose_file or _get_default_compose_file()
115+
project_name = project_name or _get_default_compose_project_name()
116+
117+
command = ['docker', 'compose']
118+
if project_name:
119+
command.extend(['-p', project_name])
120+
command.extend(['-f', os.fspath(compose_file), 'ps', '-q', compose_service])
121+
122+
container_id = run_command(command, capture='out', check=True).stdout.strip()
123+
if not container_id:
124+
raise AssertionError(f'No container found for compose service {compose_service!r}')
125+
126+
return container_id
127+
128+
129+
def _get_default_compose_service() -> str:
130+
docker_metadata = get_state('docker_compose_metadata', {})
131+
if docker_metadata.get('service_name'):
132+
return docker_metadata['service_name']
133+
134+
return os.path.basename(find_check_root(depth=2))
135+
136+
137+
def _get_default_compose_file() -> str:
138+
docker_metadata = get_state('docker_compose_metadata', {})
139+
if docker_metadata.get('compose_file'):
140+
return docker_metadata['compose_file']
141+
142+
compose_file = os.path.join(find_check_root(depth=3), 'tests', 'docker', 'docker-compose.yml')
143+
if os.path.exists(compose_file):
144+
return compose_file
145+
146+
raise AssertionError(
147+
'Could not determine the compose file. Pass compose_file explicitly or use docker_run with a compose file.'
148+
)
149+
150+
151+
def _get_default_compose_project_name() -> str | None:
152+
docker_metadata = get_state('docker_compose_metadata', {})
153+
return docker_metadata.get('project_name') or os.getenv('COMPOSE_PROJECT_NAME')
154+
155+
156+
def _inspect_container(container_id: str) -> dict[str, Any]:
157+
raw_inspect = run_command(['docker', 'inspect', container_id], capture='out', check=True).stdout
158+
return json.loads(raw_inspect)[0]
159+
160+
161+
def _get_container_ip_from_inspect(inspect_data: Mapping[str, Any]) -> str:
162+
networks = inspect_data.get('NetworkSettings', {}).get('Networks', {})
163+
for network in networks.values():
164+
ip_address = network.get('IPAddress')
165+
if ip_address:
166+
return ip_address
167+
168+
raise AssertionError(f"Could not determine container IP for {inspect_data.get('Name', '<unknown>')}")
169+
170+
171+
def _get_container_ports(inspect_data: Mapping[str, Any]) -> list[int]:
172+
ports: set[int] = set()
173+
exposed_ports = inspect_data.get('Config', {}).get('ExposedPorts') or {}
174+
network_ports = inspect_data.get('NetworkSettings', {}).get('Ports') or {}
175+
176+
for raw_port in list(exposed_ports) + list(network_ports):
177+
port, _, protocol = raw_port.partition('/')
178+
if protocol and protocol != 'tcp':
179+
continue
180+
try:
181+
ports.add(int(port))
182+
except ValueError:
183+
continue
184+
185+
if not ports:
186+
raise AssertionError(f"No TCP ports found for container {inspect_data.get('Name', '<unknown>')}")
187+
188+
return sorted(ports)
189+
190+
191+
def _get_container_logs(container_id: str) -> str:
192+
result = run_command(['docker', 'logs', container_id], capture=True)
193+
return result.stdout + result.stderr
194+
195+
196+
def _assert_container_stable(
197+
initial_state: Mapping[str, Any], current_state: Mapping[str, Any], candidate_index: int
198+
) -> None:
199+
initial_id = initial_state['Id']
200+
current_id = current_state['Id']
201+
if current_id != initial_id:
202+
raise AssertionError(
203+
f'Container changed while probing candidate #{candidate_index}: {initial_id} -> {current_id}'
204+
)
205+
206+
state = current_state.get('State', {})
207+
if not state.get('Running'):
208+
raise AssertionError(f'Container is not running after probing candidate #{candidate_index}')
209+
210+
initial_restart_count = initial_state.get('RestartCount', 0)
211+
current_restart_count = current_state.get('RestartCount', 0)
212+
if current_restart_count != initial_restart_count:
213+
raise AssertionError(
214+
f'Container restart count changed while probing candidate #{candidate_index}: '
215+
f'{initial_restart_count} -> {current_restart_count}'
216+
)
217+
218+
health = state.get('Health')
219+
if health and health.get('Status') != 'healthy':
220+
raise AssertionError(f"Container health is {health.get('Status')!r} after probing candidate #{candidate_index}")
221+
222+
223+
def _assert_no_log_patterns(logs: str, patterns: Sequence[str], candidate_index: int) -> None:
224+
for pattern in patterns:
225+
match = re.search(pattern, logs, re.IGNORECASE)
226+
if match:
227+
raise AssertionError(
228+
f'Container logs matched {pattern!r} after probing candidate #{candidate_index}: {match.group(0)!r}'
229+
)
230+
231+
232+
def get_e2e_discovery_metadata(
233+
check_root: str | os.PathLike[str] | None = None,
234+
) -> dict[str, list[str]]:
235+
"""Return Docker volume metadata for an e2e discovery run.
236+
237+
Mounts the integration's ``auto_conf.yaml`` into the agent container.
238+
239+
Use ``dd_agent_check_discovery`` alongside this metadata so that the static
240+
per-env config is temporarily replaced with an empty-instances file, leaving
241+
``auto_conf.yaml`` as the sole AD template driving config-discovery.
242+
"""
243+
check_root = os.fspath(check_root or find_check_root(depth=1))
244+
check_name = os.path.basename(check_root)
245+
check_pkg = os.path.join(check_root, 'datadog_checks', check_name)
246+
auto_conf = os.path.join(check_pkg, 'data', 'auto_conf.yaml')
247+
248+
return {
249+
'docker_volumes': [
250+
f'{auto_conf}:/etc/datadog-agent/conf.d/{check_name}.d/auto_conf.yaml:ro',
251+
'/var/run/docker.sock:/var/run/docker.sock:ro',
252+
],
253+
}
254+
255+
45256
def compose_file_active(compose_file):
46257
"""
47258
Returns a `bool` indicating whether or not a compose file has any active services.
@@ -221,6 +432,16 @@ def docker_run(
221432
'mount_logs: expected True, a list or a set, but got {}'.format(type(mount_logs).__name__)
222433
)
223434

435+
if compose_file is not None:
436+
save_state(
437+
'docker_compose_metadata',
438+
{
439+
'compose_file': compose_file,
440+
'project_name': (env_vars or {}).get('COMPOSE_PROJECT_NAME') or os.getenv('COMPOSE_PROJECT_NAME'),
441+
'service_name': service_name,
442+
},
443+
)
444+
224445
with environment_run(
225446
up=set_up,
226447
down=tear_down,

datadog_checks_dev/datadog_checks/dev/plugin/pytest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,28 @@ def run_check(config=None, **kwargs):
237237
yield run_check
238238

239239

240+
@pytest.fixture
241+
def dd_agent_check_discovery(dd_agent_check):
242+
"""Wrapper around ``dd_agent_check`` for config-discovery e2e tests.
243+
244+
Passes the empty-instances config required to let ``auto_conf.yaml`` drive
245+
autodiscovery, and sets sensible defaults for ``discovery_min_instances`` and
246+
``discovery_timeout`` — all of which can be overridden per call.
247+
"""
248+
if not e2e_testing():
249+
pytest.skip('Not running E2E tests')
250+
251+
def run(*, discovery_min_instances=1, discovery_timeout=30, **kwargs):
252+
return dd_agent_check(
253+
{'init_config': {}, 'instances': []},
254+
discovery_min_instances=discovery_min_instances,
255+
discovery_timeout=discovery_timeout,
256+
**kwargs,
257+
)
258+
259+
return run
260+
261+
240262
@pytest.fixture
241263
def dd_run_check():
242264
checks = {}

datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def models(ctx, check, sync, verbose):
166166
current_model_file_lines = read_file_lines(model_file_path)
167167

168168
if model_file in CUSTOM_FILES and (len(current_model_file_lines) + 1) > len(license_header_lines):
169-
# validators.py and deprecations.py are custom files, they should only be rendered the first time
169+
# Custom files should only be rendered the first time.
170170
continue
171171

172172
if not is_community_check:

0 commit comments

Comments
 (0)