diff --git a/traefik_mesh/assets/configuration/spec.yaml b/traefik_mesh/assets/configuration/spec.yaml index 065d2f5d3330b..e3f4b8cb64846 100644 --- a/traefik_mesh/assets/configuration/spec.yaml +++ b/traefik_mesh/assets/configuration/spec.yaml @@ -2,6 +2,15 @@ name: Traefik Mesh fleet_configurable: true files: - name: traefik_mesh.yaml + discovery: + strategies: + - strategy: local:from_traefik_mesh_kube_daemon_set + provides: + - endpoints + inputs: {} + candidates: + - openmetrics_endpoint: "{endpoints.openmetrics_endpoint}" + traefik_proxy_api_endpoint: "{endpoints.traefik_proxy_api_endpoint}" options: - template: init_config options: @@ -21,7 +30,7 @@ files: required: false description: URL of the Traefik Mesh proxy to query. value: - example: http://: + example: HTTP://: pattern: \w+ type: string - name: traefik_controller_api_endpoint @@ -30,6 +39,13 @@ files: required: false description: URL of the Traefik Mesh controller to query. value: - example: http://: + example: HTTP://: pattern: \w+ type: string + - name: auto_conf.yaml + options: + - template: ad_identifiers + overrides: + value.example: + - traefik + - template: auto_conf/discovery diff --git a/traefik_mesh/changelog.d/24637.added b/traefik_mesh/changelog.d/24637.added new file mode 100644 index 0000000000000..1455667b0b73f --- /dev/null +++ b/traefik_mesh/changelog.d/24637.added @@ -0,0 +1 @@ +Add container-based config discovery support. diff --git a/traefik_mesh/changelog.d/24637.fixed b/traefik_mesh/changelog.d/24637.fixed new file mode 100644 index 0000000000000..241595a9a5bfe --- /dev/null +++ b/traefik_mesh/changelog.d/24637.fixed @@ -0,0 +1 @@ +Fix a bug where an unset optional endpoint field could crash the check with an invalid URL error. diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/__init__.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/__init__.py index 106fff2032f68..5c2bf5c9f46d4 100644 --- a/traefik_mesh/datadog_checks/traefik_mesh/config_models/__init__.py +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/__init__.py @@ -1,7 +1,3 @@ -# (C) Datadog, Inc. 2024-present -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) - # This file is autogenerated. # To change this file you should edit assets/configuration/spec.yaml and then run the following commands: # ddev -x validate config -s diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/defaults.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/defaults.py index 9d2467c34bb8e..d100f53aede09 100644 --- a/traefik_mesh/datadog_checks/traefik_mesh/config_models/defaults.py +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/defaults.py @@ -1,7 +1,3 @@ -# (C) Datadog, Inc. 2024-present -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) - # This file is autogenerated. # To change this file you should edit assets/configuration/spec.yaml and then run the following commands: # ddev -x validate config -s @@ -124,14 +120,6 @@ def instance_tls_verify(): return True -def instance_traefik_controller_api_endpoint(): - return 'http://:' - - -def instance_traefik_proxy_api_endpoint(): - return 'http://:' - - def instance_use_latest_spec(): return False diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery.py new file mode 100644 index 0000000000000..87e83bb499074 --- /dev/null +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery.py @@ -0,0 +1,39 @@ +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + +from datadog_checks.base.utils.discovery import Service +from datadog_checks.traefik_mesh.config_models import discovery_overrides +from datadog_checks.traefik_mesh.config_models.discovery_strategies import from_traefik_mesh_kube_daemon_set +from datadog_checks.traefik_mesh.config_models.instance import InstanceConfig +from datadog_checks.traefik_mesh.config_models.shared import SharedConfig + + +def _generated_candidates(service: Service) -> Iterator[dict[str, Any]]: + shared = SharedConfig.model_validate({}, context={'configured_fields': frozenset()}).model_dump( + by_alias=True, mode='json', exclude_none=True + ) + # discovery[0]: local:from_traefik_mesh_kube_daemon_set + for ctx in from_traefik_mesh_kube_daemon_set(service): + instance_data = { + 'openmetrics_endpoint': '{endpoints.openmetrics_endpoint}'.format(service=service, **ctx), + 'traefik_proxy_api_endpoint': '{endpoints.traefik_proxy_api_endpoint}'.format(service=service, **ctx), + } + instance = InstanceConfig.model_validate( + instance_data, context={'configured_fields': frozenset(instance_data)} + ).model_dump(by_alias=True, mode='json', exclude_none=True) + yield {'init_config': shared, 'instances': [instance]} + + +def candidates(service: Service) -> Iterator[dict[str, Any]]: + override = getattr(discovery_overrides, 'candidates', None) + if override is None: + yield from _generated_candidates(service) + else: + yield from override(service, default=_generated_candidates) diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery_overrides.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery_overrides.py new file mode 100644 index 0000000000000..1f772c5e86601 --- /dev/null +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery_overrides.py @@ -0,0 +1,8 @@ +# Override the generated discovery candidates() for this integration. +# +# Define a candidates(service, default) function to wrap or replace the generated +# candidate generation. `default` is the generated generator; call it to reuse +# the spec-driven candidates, or ignore it to replace them entirely. +# +# def candidates(service, default): +# yield from default(service) diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery_strategies.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery_strategies.py new file mode 100644 index 0000000000000..232b7363f3477 --- /dev/null +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/discovery_strategies.py @@ -0,0 +1,50 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + +from datadog_checks.base.utils.discovery import Service, discovery_strategy +from datadog_checks.base.utils.tagging import tagger + +TRAEFIK_MESH_PROXY_DAEMON_SET = 'traefik-mesh-proxy' +TRAEFIK_MESH_PROXY_METRICS_PORT = 8080 + + +@dataclass(frozen=True) +class TraefikMeshDiscoveryEndpoints: + openmetrics_endpoint: str = '' + traefik_proxy_api_endpoint: str = '' + + +def container_tagger_entity_id(container_id: str) -> str: + """Return the tagger entity ID for a Kubernetes container runtime ID.""" + if container_id and '://' in container_id: + return '://'.join(('container_id', container_id.split('://', 1)[1])) + + return container_id + + +@discovery_strategy(provides=('endpoints',)) +def from_traefik_mesh_kube_daemon_set(service: Service) -> Iterator[dict[str, TraefikMeshDiscoveryEndpoints]]: + """Yield the proxy's metrics/API endpoint for a matching Traefik Mesh proxy container. + + The proxy runs the stock upstream ``traefik`` image, which is indistinguishable from a plain + (non-mesh) Traefik reverse-proxy deployment at the image level. The Traefik Mesh Helm chart + hardcodes the proxy's DaemonSet name to ``traefik-mesh-proxy`` though, so gating on that + Kubernetes-derived tag (rather than the image) avoids matching an unrelated Traefik deployment. + """ + tags = tagger.tag(container_tagger_entity_id(service.id), tagger.LOW) or [] + if f'kube_daemon_set:{TRAEFIK_MESH_PROXY_DAEMON_SET}' not in tags: + return + + base_url = f'http://{service.host}:{TRAEFIK_MESH_PROXY_METRICS_PORT}' + yield { + 'endpoints': TraefikMeshDiscoveryEndpoints( + openmetrics_endpoint=f'{base_url}/metrics', + traefik_proxy_api_endpoint=base_url, + ) + } diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/instance.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/instance.py index 5eb1804fafe0a..16cb94ea148cc 100644 --- a/traefik_mesh/datadog_checks/traefik_mesh/config_models/instance.py +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/instance.py @@ -1,7 +1,3 @@ -# (C) Datadog, Inc. 2024-present -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) - # This file is autogenerated. # To change this file you should edit assets/configuration/spec.yaml and then run the following commands: # ddev -x validate config -s diff --git a/traefik_mesh/datadog_checks/traefik_mesh/config_models/shared.py b/traefik_mesh/datadog_checks/traefik_mesh/config_models/shared.py index 0e8a9ecab10a2..da933d6d8ab3f 100644 --- a/traefik_mesh/datadog_checks/traefik_mesh/config_models/shared.py +++ b/traefik_mesh/datadog_checks/traefik_mesh/config_models/shared.py @@ -1,7 +1,3 @@ -# (C) Datadog, Inc. 2024-present -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) - # This file is autogenerated. # To change this file you should edit assets/configuration/spec.yaml and then run the following commands: # ddev -x validate config -s diff --git a/traefik_mesh/datadog_checks/traefik_mesh/data/auto_conf.yaml b/traefik_mesh/datadog_checks/traefik_mesh/data/auto_conf.yaml new file mode 100644 index 0000000000000..4b763a965a98a --- /dev/null +++ b/traefik_mesh/datadog_checks/traefik_mesh/data/auto_conf.yaml @@ -0,0 +1,19 @@ +## @param ad_identifiers - list of strings - required +## A list of container identifiers that are used by Autodiscovery to identify +## which container the check should be run against. For more information, see: +## https://docs.datadoghq.com/agent/guide/ad_identifiers/ +# +ad_identifiers: + - traefik + +## Enables configuration discovery +# +discovery: {} + +## Unused init configuration +# +init_config: + +## Unused instance configuration +# +instances: [] diff --git a/traefik_mesh/datadog_checks/traefik_mesh/data/conf.yaml.example b/traefik_mesh/datadog_checks/traefik_mesh/data/conf.yaml.example index d672fe162de46..fb58c5b723583 100644 --- a/traefik_mesh/datadog_checks/traefik_mesh/data/conf.yaml.example +++ b/traefik_mesh/datadog_checks/traefik_mesh/data/conf.yaml.example @@ -642,12 +642,12 @@ instances: # exclude: # - - ## @param traefik_proxy_api_endpoint - string - optional - default: http://: + ## @param traefik_proxy_api_endpoint - string - optional - default: HTTP://: ## URL of the Traefik Mesh proxy to query. # - # traefik_proxy_api_endpoint: http://: + # traefik_proxy_api_endpoint: HTTP://: - ## @param traefik_controller_api_endpoint - string - optional - default: http://: + ## @param traefik_controller_api_endpoint - string - optional - default: HTTP://: ## URL of the Traefik Mesh controller to query. # - # traefik_controller_api_endpoint: http://: + # traefik_controller_api_endpoint: HTTP://: diff --git a/traefik_mesh/pyproject.toml b/traefik_mesh/pyproject.toml index 6f8571e8b9660..d4d0d3fe6bea2 100644 --- a/traefik_mesh/pyproject.toml +++ b/traefik_mesh/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ "Topic :: System :: Monitoring", ] dependencies = [ - "datadog-checks-base>=37.33.0", + "datadog-checks-base>=37.41.0", ] dynamic = [ "version", diff --git a/traefik_mesh/tests/conftest.py b/traefik_mesh/tests/conftest.py index 53a66ad41368c..f46ecd17ffc1a 100644 --- a/traefik_mesh/tests/conftest.py +++ b/traefik_mesh/tests/conftest.py @@ -9,6 +9,7 @@ from datadog_checks.dev import get_here from datadog_checks.dev.kind import kind_run +from datadog_checks.dev.kube_discovery import setup_discovery_agent from datadog_checks.dev.kube_port_forward import port_forward from datadog_checks.dev.subprocess import run_command @@ -48,6 +49,8 @@ def setup_traefik_mesh(): @pytest.fixture(scope='session') def dd_environment(dd_save_state): with kind_run(conditions=[setup_traefik_mesh]) as kubeconfig: + setup_discovery_agent(kubeconfig) + with ExitStack() as stack: traefik_controller_api_url, traefik_controller_api_port = stack.enter_context( port_forward(kubeconfig, 'traefik-mesh', 9000, 'service', 'traefik-mesh-controller') diff --git a/traefik_mesh/tests/kind/traefik_mesh.yaml b/traefik_mesh/tests/kind/traefik_mesh.yaml index d4d6edf40b411..72b7b866ba6b0 100644 --- a/traefik_mesh/tests/kind/traefik_mesh.yaml +++ b/traefik_mesh/tests/kind/traefik_mesh.yaml @@ -480,7 +480,7 @@ spec: - "--entryPoints.udp-15022.address=:15022/udp" - "--entryPoints.udp-15023.address=:15023/udp" - "--entryPoints.udp-15024.address=:15024/udp" - - "--providers.http.endpoint=http://traefik-mesh-controller.default.svc.cluster.local:9000/api/configuration/current" + - "--providers.http.endpoint=http://traefik-mesh-controller.traefik-mesh.svc.cluster.local:9000/api/configuration/current" - "--api.dashboard=false" - "--api.insecure" - "--ping" diff --git a/traefik_mesh/tests/test_discovery.py b/traefik_mesh/tests/test_discovery.py new file mode 100644 index 0000000000000..ec58261d7e839 --- /dev/null +++ b/traefik_mesh/tests/test_discovery.py @@ -0,0 +1,84 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from __future__ import annotations + +from collections.abc import Iterator + +import pytest + +from datadog_checks.base.stubs import tagger +from datadog_checks.base.utils.discovery import Service +from datadog_checks.traefik_mesh.config_models import discovery +from datadog_checks.traefik_mesh.config_models.discovery_strategies import from_traefik_mesh_kube_daemon_set + + +@pytest.fixture(autouse=True) +def reset_tagger() -> Iterator[None]: + tagger.reset() + yield + tagger.reset() + + +def build_service(service_id: str = 'docker://abc', host: str = '10.0.0.1') -> Service: + return Service(id=service_id, host=host, ports=()) + + +def test_from_traefik_mesh_kube_daemon_set_yields_endpoint_on_matching_daemon_set() -> None: + tagger.set_tags({'container_id://abc': ['kube_daemon_set:traefik-mesh-proxy']}) + + contexts = list(from_traefik_mesh_kube_daemon_set(build_service(host='10.0.0.8'))) + + assert len(contexts) == 1 + assert contexts[0]['endpoints'].openmetrics_endpoint == 'http://10.0.0.8:8080/metrics' + assert contexts[0]['endpoints'].traefik_proxy_api_endpoint == 'http://10.0.0.8:8080' + + +@pytest.mark.parametrize( + 'tags', + [ + pytest.param([], id='missing_kube_daemon_set'), + # A plain, non-mesh Traefik reverse-proxy deployment runs the same image but is not the + # Traefik Mesh proxy DaemonSet, so it must not be discovered. + pytest.param(['kube_daemon_set:traefik'], id='different_daemon_set'), + pytest.param(['kube_deployment:traefik-mesh-proxy'], id='deployment_not_daemon_set'), + pytest.param(['pod_name:traefik-mesh-proxy-abcde'], id='no_workload_tag'), + ], +) +def test_from_traefik_mesh_kube_daemon_set_ignores_missing_or_different_daemon_sets(tags: list[str]) -> None: + tagger.set_tags({'container_id://abc': tags}) + + assert list(from_traefik_mesh_kube_daemon_set(build_service())) == [] + + +@pytest.mark.parametrize( + 'service_id', + [ + pytest.param('docker://abc', id='docker'), + pytest.param('containerd://abc', id='containerd'), + pytest.param('cri-o://abc', id='cri_o'), + pytest.param('container_id://abc', id='container_id'), + ], +) +def test_from_traefik_mesh_kube_daemon_set_queries_tagger_container_entity(service_id: str) -> None: + tagger.set_tags({'container_id://abc': ['kube_daemon_set:traefik-mesh-proxy']}) + + assert len(list(from_traefik_mesh_kube_daemon_set(build_service(service_id=service_id)))) == 1 + tagger.assert_called('container_id://abc', tagger.LOW) + + +def test_generated_discovery_matches_on_daemon_set_tag() -> None: + tagger.set_tags({'container_id://proxy': ['kube_daemon_set:traefik-mesh-proxy']}) + + candidates = list(discovery.candidates(build_service(service_id='docker://proxy', host='10.0.0.5'))) + + assert len(candidates) == 1 + instance = candidates[0]['instances'][0] + assert instance['openmetrics_endpoint'] == 'http://10.0.0.5:8080/metrics' + assert instance['traefik_proxy_api_endpoint'] == 'http://10.0.0.5:8080' + + +def test_generated_discovery_ignores_plain_traefik_deployment() -> None: + tagger.set_tags({'container_id://traefik': ['kube_deployment:traefik']}) + + assert list(discovery.candidates(build_service(service_id='docker://traefik', host='10.0.0.6'))) == [] diff --git a/traefik_mesh/tests/test_e2e.py b/traefik_mesh/tests/test_e2e.py index e2f7178890837..4b4455c042615 100644 --- a/traefik_mesh/tests/test_e2e.py +++ b/traefik_mesh/tests/test_e2e.py @@ -1,6 +1,16 @@ # (C) Datadog, Inc. 2024-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) +from typing import Any + +import pytest + +from datadog_checks.base.stubs import tagger +from datadog_checks.dev.kube_discovery import ( + assert_all_discovery_candidates_stable_kubernetes, + run_discovery_check_kubernetes, +) +from datadog_checks.traefik_mesh import TraefikMeshCheck def test_e2e_openmetrics_v2(dd_agent_check): @@ -8,3 +18,35 @@ def test_e2e_openmetrics_v2(dd_agent_check): aggregator.assert_service_check('traefik_mesh.openmetrics.health') aggregator.assert_service_check('traefik_mesh.controller.ready') + + +@pytest.mark.e2e +def test_e2e_discovery_all_candidates(aggregator: Any, datadog_agent: Any) -> None: + # The proxy's DaemonSet name (rather than its image, shared with plain Traefik deployments) is + # what the discovery strategy keys on, so stub the tag the same way the real Agent's Kubernetes + # tagger would derive it from the pod's DaemonSet owner reference. + tagger.set_tags({'container_id://traefik-mesh-proxy': ['kube_daemon_set:traefik-mesh-proxy']}) + try: + assert_all_discovery_candidates_stable_kubernetes( + TraefikMeshCheck, + aggregator, + datadog_agent, + namespace='traefik-mesh', + pod_selector='component=maesh-mesh', + service_id='docker://traefik-mesh-proxy', + ) + finally: + tagger.reset() + + +@pytest.mark.e2e +def test_e2e_discovery(aggregator: Any, datadog_agent: Any) -> None: + aggregator = run_discovery_check_kubernetes( + aggregator, + datadog_agent, + check_rate=True, + discovery_min_instances=1, + discovery_timeout=60, + ) + + aggregator.assert_service_check('traefik_mesh.openmetrics.health')