Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions datadog_checks_dev/changelog.d/24238.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add process autodiscovery E2E testing helpers.
44 changes: 34 additions & 10 deletions datadog_checks_dev/datadog_checks/dev/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,29 +229,53 @@ def _assert_no_log_patterns(logs: str, patterns: Sequence[str], candidate_index:
)


def _get_auto_conf_volume(check_root: str | os.PathLike[str] | None = None) -> str:
check_root = os.fspath(check_root or find_check_root(depth=2))
check_name = os.path.basename(check_root)
check_pkg = os.path.join(check_root, 'datadog_checks', check_name)
return f'{check_pkg}/data/auto_conf.yaml:/etc/datadog-agent/conf.d/{check_name}.d/auto_conf.yaml:ro'


def get_e2e_discovery_metadata(
check_root: str | os.PathLike[str] | None = None,
) -> dict[str, list[str]]:
"""Return Docker volume metadata for an e2e discovery run.
*,
process: bool = False,
) -> dict[str, list[str] | dict[str, str]]:
"""Return metadata for an e2e discovery run.

Mounts the integration's ``auto_conf.yaml`` into the agent container.
Mounts the integration's ``auto_conf.yaml`` into the agent container. Pass
``process=True`` to also grant the capabilities needed for process-based
autodiscovery to see processes running in sibling containers (on top of the
container-based autodiscovery already enabled via the Docker socket mount).

Use ``dd_agent_check_discovery`` alongside this metadata so that the static
per-env config is temporarily replaced with an empty-instances file, leaving
``auto_conf.yaml`` as the sole AD template driving config-discovery.
"""
check_root = os.fspath(check_root or find_check_root(depth=1))
check_name = os.path.basename(check_root)
check_pkg = os.path.join(check_root, 'datadog_checks', check_name)
auto_conf = os.path.join(check_pkg, 'data', 'auto_conf.yaml')

return {
metadata: dict[str, list[str] | dict[str, str]] = {
'docker_volumes': [
f'{auto_conf}:/etc/datadog-agent/conf.d/{check_name}.d/auto_conf.yaml:ro',
_get_auto_conf_volume(check_root),
'/var/run/docker.sock:/var/run/docker.sock:ro',
],
}

if process:
metadata['env_vars'] = {
# Reduce the default service collection interval and minimum process
# age to speed up tests. This needs to be set on the container
# instead of being passed in to `agent check` since it's read by the
# system-probe(-lite) daemon.
'DD_DISCOVERY_SERVICE_COLLECTION_INTERVAL': '5s',
'DD_DISCOVERY_SERVICE_COLLECTION_MIN_PROCESS_AGE': '1s',
}
# system-probe(-lite) needs these capabilities to read /proc entries of
# other processes for service discovery. Without them it falls back to
# only scanning the agent's own PID namespace, which finds no host
# processes.
metadata['cap_add'] = ['SYS_PTRACE', 'DAC_READ_SEARCH']

return metadata


def compose_file_active(compose_file):
"""
Expand Down
64 changes: 61 additions & 3 deletions datadog_checks_dev/datadog_checks/dev/plugin/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from base64 import urlsafe_b64encode
from collections import namedtuple # Not using dataclasses for Py2 compatibility
from io import open
from typing import Dict, List, Literal, Optional, Tuple, overload # noqa: F401
from typing import Any, Dict, List, Literal, Optional, Tuple, overload # noqa: F401

import pytest

Expand Down Expand Up @@ -203,6 +203,9 @@ def run_check(config=None, **kwargs):
if 'times' in kwargs:
kwargs['check_times'] = kwargs.pop('times')

for env_key, env_value in (kwargs.pop('env_vars', None) or {}).items():
check_command.extend(['--env', '{}={}'.format(env_key, env_value)])

for key, value in kwargs.items():
if value is not False:
check_command.append('--{}'.format(key.replace('_', '-')))
Expand Down Expand Up @@ -237,25 +240,80 @@ def run_check(config=None, **kwargs):
yield run_check


CONTAINER_ORIGIN_TAG_PREFIXES = ('docker_image:', 'image_id:', 'image_name:', 'image_tag:', 'short_image:')


def assert_discovery_used_expected_mechanism(aggregator: Any, *, process: bool) -> None:
"""Assert that a ``dd_agent_check_discovery`` run actually used the mechanism it claims to.

Container-based Autodiscovery has its instances tagged by the Agent's tagger with container-origin
tags (``docker_image``, ``image_name``, etc.); process-based Autodiscovery does not. Without this
check, a bug that fails to exclude the ``docker`` feature during a ``process=True`` run can go
unnoticed: the container-based path silently produces a working instance, leaving the process CEL
selector completely untested even though the test still passes.
"""
tags = set()
has_metrics = False
for name in aggregator.metric_names:
for metric in aggregator.metrics(name):
has_metrics = True
tags.update(metric.tags)

# If no data was submitted, don't assert anything here, let the test fail
# due to other assertions in the main test function.
if not has_metrics:
return

discovered_via_container = any(tag.startswith(prefix) for tag in tags for prefix in CONTAINER_ORIGIN_TAG_PREFIXES)

if process:
assert not discovered_via_container, (
'Process-based discovery was requested, but the submitted metrics carry container-origin '
'tags. Autodiscovery actually matched the container instead of the process, so the process '
'CEL selector was never exercised.'
)
else:
assert discovered_via_container, (
'Container-based discovery was requested, but the submitted metrics carry no container-origin '
'tags, so it is unclear that Autodiscovery actually matched via the container path.'
)


@pytest.fixture
def dd_agent_check_discovery(dd_agent_check):
"""Wrapper around ``dd_agent_check`` for config-discovery e2e tests.

Passes the empty-instances config required to let ``auto_conf.yaml`` drive
autodiscovery, and sets sensible defaults for ``discovery_min_instances`` and
``discovery_timeout`` — all of which can be overridden per call.

Pass ``process=True`` to exercise process-based (rather than container-based)
autodiscovery for a process that is otherwise container-bound.
"""
if not e2e_testing():
pytest.skip('Not running E2E tests')

def run(*, discovery_min_instances=1, discovery_timeout=30, **kwargs):
return dd_agent_check(
def run(*, discovery_min_instances=1, discovery_timeout=30, process=False, **kwargs):
if process:
env_vars = kwargs.pop('env_vars', None) or {}
# Exclude the `docker` feature so this run's autodiscovery never
# learns about any containers. This is because process-based
# autodiscovery normally ignores processes if it knows that they are
# inside containers, and we need to override this behavior.
env_vars.setdefault('DD_AUTOCONFIG_EXCLUDE_FEATURES', 'docker')
kwargs['env_vars'] = env_vars

aggregator = dd_agent_check(
{'init_config': {}, 'instances': []},
discovery_min_instances=discovery_min_instances,
discovery_timeout=discovery_timeout,
**kwargs,
)

assert_discovery_used_expected_mechanism(aggregator, process=process)

return aggregator

return run


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- name: cel_selector
description: CEL selector for autodiscovery.
enabled: true
example: {}
21 changes: 0 additions & 21 deletions datadog_checks_dev/tests/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
assert_all_discovery_candidates_stable,
compose_file_active,
docker_run,
get_e2e_discovery_metadata,
)
from datadog_checks.dev.subprocess import run_command

Expand Down Expand Up @@ -50,26 +49,6 @@ def _container_inspect(*, restart_count=0, running=True, health='healthy', ports
}


def test_get_e2e_discovery_metadata(tmp_path):
check_root = tmp_path / 'test_check'
check_package_root = check_root / 'datadog_checks' / 'test_check'
data_dir = check_package_root / 'data'
data_dir.mkdir(parents=True)
(data_dir / 'auto_conf.yaml').write_text(
'ad_identifiers:\n - test\ndiscovery: {}\ninit_config:\ninstances: []\n',
encoding='utf-8',
)

metadata = get_e2e_discovery_metadata(check_root)

assert metadata == {
'docker_volumes': [
f'{check_package_root}/data/auto_conf.yaml:/etc/datadog-agent/conf.d/test_check.d/auto_conf.yaml:ro',
'/var/run/docker.sock:/var/run/docker.sock:ro',
],
}


class TestComposeFileActive:
def test_down(self):
compose_file = os.path.join(DOCKER_DIR, 'test_default.yaml')
Expand Down
1 change: 1 addition & 0 deletions ddev/changelog.d/24238.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pass capabilities from e2e metadata to the Agent container.
3 changes: 3 additions & 0 deletions ddev/src/ddev/e2e/agent/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ def start(self, *, agent_build: str | None, local_packages: dict[Path, str], env
for key, value in sorted(env_vars.items()):
command.extend(['-e', f'{key}={value}'])

for cap in self.metadata.get('cap_add', []):
command.extend(['--cap-add', cap])

# The docker `--add-host` command will reliably create entries in the `/etc/hosts` file,
# otherwise, edits to that file will be overwritten on container restarts
for host, ip in self.metadata.get('custom_hosts', []):
Expand Down
5 changes: 5 additions & 0 deletions krakend/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ files:
overrides:
value.example:
- krakend
- template: auto_conf/cel_selector
overrides:
cel_selector.example:
processes:
- "process.name == 'krakend'"
- template: auto_conf/discovery
1 change: 1 addition & 0 deletions krakend/changelog.d/24238.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add process autodiscovery support.
6 changes: 6 additions & 0 deletions krakend/datadog_checks/krakend/data/auto_conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
ad_identifiers:
- krakend

## CEL selector for autodiscovery.
#
cel_selector:
processes:
- process.name == 'krakend'

## Enables configuration discovery
#
discovery: {}
Expand Down
2 changes: 1 addition & 1 deletion krakend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def run_docker_e2e(env_vars: dict[str, str], conditions: list[LazyFunction]):
{
"instances": [{"openmetrics_endpoint": OPEN_METRICS_ENDPOINT}],
},
get_e2e_discovery_metadata(),
get_e2e_discovery_metadata(process=True),
)


Expand Down
5 changes: 3 additions & 2 deletions krakend/tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ def test_e2e(dd_agent_check, instance: InstanceBuilder):


@pytest.mark.e2e
def test_e2e_discovery(dd_agent_check_discovery, is_lab):
@pytest.mark.parametrize('process', [False, True], ids=['container', 'process'])
def test_e2e_discovery(dd_agent_check_discovery, is_lab, process):
# In the lab environment we currently do not mount auto_conf.yaml into the
# Agent container, so the Agent has no Autodiscovery template to trigger
# config discovery.
if is_lab:
pytest.skip('lab does not currently support configuration discovery')

aggregator = dd_agent_check_discovery(check_rate=True)
aggregator = dd_agent_check_discovery(check_rate=True, process=process)

metadata_metrics = get_metrics_from_metadata()

Expand Down
Loading