Skip to content
Closed
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
20 changes: 18 additions & 2 deletions traefik_mesh/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -21,7 +30,7 @@ files:
required: false
description: URL of the Traefik Mesh proxy to query.
value:
example: http://<PROXY_ENDPOINT>:<API_PORT>
example: HTTP://<PROXY_ENDPOINT>:<API_PORT>
pattern: \w+
type: string
- name: traefik_controller_api_endpoint
Expand All @@ -30,6 +39,13 @@ files:
required: false
description: URL of the Traefik Mesh controller to query.
value:
example: http://<CONTROLLER_ENDPOINT>:<API_PORT>
example: HTTP://<CONTROLLER_ENDPOINT>:<API_PORT>
pattern: \w+
type: string
- name: auto_conf.yaml
options:
- template: ad_identifiers
overrides:
value.example:
- traefik
- template: auto_conf/discovery
1 change: 1 addition & 0 deletions traefik_mesh/changelog.d/24637.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add container-based config discovery support.
1 change: 1 addition & 0 deletions traefik_mesh/changelog.d/24637.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug where an unset optional endpoint field could crash the check with an invalid URL error.
Original file line number Diff line number Diff line change
@@ -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 <INTEGRATION_NAME>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <INTEGRATION_NAME>
Expand Down Expand Up @@ -124,14 +120,6 @@ def instance_tls_verify():
return True


def instance_traefik_controller_api_endpoint():
return 'http://<CONTROLLER_ENDPOINT>:<API_PORT>'


def instance_traefik_proxy_api_endpoint():
return 'http://<PROXY_ENDPOINT>:<API_PORT>'


def instance_use_latest_spec():
return False

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>

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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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 <INTEGRATION_NAME>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <INTEGRATION_NAME>
Expand Down
19 changes: 19 additions & 0 deletions traefik_mesh/datadog_checks/traefik_mesh/data/auto_conf.yaml
Original file line number Diff line number Diff line change
@@ -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: []
Original file line number Diff line number Diff line change
Expand Up @@ -642,12 +642,12 @@ instances:
# exclude:
# - <EXCLUDE_REGEX>

## @param traefik_proxy_api_endpoint - string - optional - default: http://<PROXY_ENDPOINT>:<API_PORT>
## @param traefik_proxy_api_endpoint - string - optional - default: HTTP://<PROXY_ENDPOINT>:<API_PORT>
## URL of the Traefik Mesh proxy to query.
#
# traefik_proxy_api_endpoint: http://<PROXY_ENDPOINT>:<API_PORT>
# traefik_proxy_api_endpoint: HTTP://<PROXY_ENDPOINT>:<API_PORT>

## @param traefik_controller_api_endpoint - string - optional - default: http://<CONTROLLER_ENDPOINT>:<API_PORT>
## @param traefik_controller_api_endpoint - string - optional - default: HTTP://<CONTROLLER_ENDPOINT>:<API_PORT>
## URL of the Traefik Mesh controller to query.
#
# traefik_controller_api_endpoint: http://<CONTROLLER_ENDPOINT>:<API_PORT>
# traefik_controller_api_endpoint: HTTP://<CONTROLLER_ENDPOINT>:<API_PORT>
2 changes: 1 addition & 1 deletion traefik_mesh/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ classifiers = [
"Topic :: System :: Monitoring",
]
dependencies = [
"datadog-checks-base>=37.33.0",
"datadog-checks-base>=37.41.0",
]
dynamic = [
"version",
Expand Down
3 changes: 3 additions & 0 deletions traefik_mesh/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion traefik_mesh/tests/kind/traefik_mesh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
84 changes: 84 additions & 0 deletions traefik_mesh/tests/test_discovery.py
Original file line number Diff line number Diff line change
@@ -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'))) == []
Loading
Loading