diff --git a/fluxcd/assets/configuration/spec.yaml b/fluxcd/assets/configuration/spec.yaml index af748939d282d..ac695fc3d4101 100644 --- a/fluxcd/assets/configuration/spec.yaml +++ b/fluxcd/assets/configuration/spec.yaml @@ -2,6 +2,12 @@ name: fluxcd fleet_configurable: true files: - name: fluxcd.yaml + discovery: + strategies: + - template: discovery/openmetrics_from_named_ports + overrides: + port_names: + - http-prom options: - template: init_config options: @@ -15,3 +21,22 @@ files: for Flux custom resources (Flux 2.1+). value: type: string +- name: auto_conf.yaml + options: + - template: ad_identifiers + overrides: + value.example: + - helm-controller + - image-automation-controller + - image-reflector-controller + - kustomize-controller + - notification-controller + - source-controller + - template: auto_conf/cel_selector + overrides: + cel_selector.example: + containers: + # Narrow down matching containers since the controller names are generic + # and don't identify the container as being part of FluxCD. + - container.image.reference.contains("fluxcd/") + - template: auto_conf/discovery diff --git a/fluxcd/changelog.d/24510.added b/fluxcd/changelog.d/24510.added new file mode 100644 index 0000000000000..1455667b0b73f --- /dev/null +++ b/fluxcd/changelog.d/24510.added @@ -0,0 +1 @@ +Add container-based config discovery support. diff --git a/fluxcd/datadog_checks/fluxcd/config_models/discovery.py b/fluxcd/datadog_checks/fluxcd/config_models/discovery.py new file mode 100644 index 0000000000000..75d49af525a70 --- /dev/null +++ b/fluxcd/datadog_checks/fluxcd/config_models/discovery.py @@ -0,0 +1,42 @@ +# (C) Datadog, Inc. 2026-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 +# 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, candidate_ports_by_name +from datadog_checks.fluxcd.config_models import discovery_overrides +from datadog_checks.fluxcd.config_models.instance import InstanceConfig +from datadog_checks.fluxcd.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]: from_named_ports + for port in candidate_ports_by_name(service, ['http-prom']): + ctx = {'port': port} + instance_data = { + 'openmetrics_endpoint': 'http://{service.host}:{port.number}/metrics'.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/fluxcd/datadog_checks/fluxcd/config_models/discovery_overrides.py b/fluxcd/datadog_checks/fluxcd/config_models/discovery_overrides.py new file mode 100644 index 0000000000000..66af68809dd4c --- /dev/null +++ b/fluxcd/datadog_checks/fluxcd/config_models/discovery_overrides.py @@ -0,0 +1,12 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# 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/fluxcd/datadog_checks/fluxcd/config_models/discovery_strategies.py b/fluxcd/datadog_checks/fluxcd/config_models/discovery_strategies.py new file mode 100644 index 0000000000000..5ac036ddb4684 --- /dev/null +++ b/fluxcd/datadog_checks/fluxcd/config_models/discovery_strategies.py @@ -0,0 +1,18 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# Here you can define custom (local:) discovery strategies for this integration. +# +# Decorate a generator with @discovery_strategy (imported from +# datadog_checks.base.utils.discovery) and reference it from the spec discovery +# stanza as `strategy: local:`. The function receives the +# discovered Service plus the inputs declared in the spec and yields one context +# (ctx) mapping per candidate, exposing the keys listed in `provides`. +# +# from datadog_checks.base.utils.discovery import discovery_strategy +# +# @discovery_strategy(provides=('svc',)) +# def from_some_config(service, config_path): +# ... +# yield {'svc': ...} diff --git a/fluxcd/datadog_checks/fluxcd/data/auto_conf.yaml b/fluxcd/datadog_checks/fluxcd/data/auto_conf.yaml new file mode 100644 index 0000000000000..2eb738fcdb0be --- /dev/null +++ b/fluxcd/datadog_checks/fluxcd/data/auto_conf.yaml @@ -0,0 +1,30 @@ +## @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: + - helm-controller + - image-automation-controller + - image-reflector-controller + - kustomize-controller + - notification-controller + - source-controller + +## CEL selector for autodiscovery. +# +cel_selector: + containers: + - container.image.reference.contains("fluxcd/") + +## Enables configuration discovery +# +discovery: {} + +## Unused init configuration +# +init_config: + +## Unused instance configuration +# +instances: [] diff --git a/fluxcd/pyproject.toml b/fluxcd/pyproject.toml index 180e6cbcaf1ef..6a09cb1c62609 100644 --- a/fluxcd/pyproject.toml +++ b/fluxcd/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ "Private :: Do Not Upload", ] dependencies = [ - "datadog-checks-base>=37.33.0", + "datadog-checks-base>=38.0.0", ] dynamic = [ "version", diff --git a/fluxcd/tests/conftest.py b/fluxcd/tests/conftest.py index 7f921066e8155..bfe31ed4b3bd8 100644 --- a/fluxcd/tests/conftest.py +++ b/fluxcd/tests/conftest.py @@ -14,6 +14,7 @@ from datadog_checks.fluxcd import FluxcdCheck HERE = get_here() +CHECK_ROOT = os.path.dirname(HERE) opj = os.path.join # The Services in flux-system (source-controller, notification-controller) only expose the @@ -25,6 +26,7 @@ CONTROLLERS = ('source-controller', 'helm-controller', 'kustomize-controller', 'notification-controller') METRICS_PORT = 8080 POD_IP_STATE_PREFIX = 'fluxcd_pod_ip_' +KUBECONFIG_STATE = 'fluxcd_kubeconfig' def setup_fluxcd(): @@ -73,6 +75,7 @@ def get_controller_pod_ip(controller: str) -> str: @pytest.fixture(scope='session') def dd_environment(): with kind_run(conditions=[setup_fluxcd]) as kubeconfig: + save_state(KUBECONFIG_STATE, kubeconfig) instances = [ { 'openmetrics_endpoint': f'http://{get_state(POD_IP_STATE_PREFIX + controller)}:{METRICS_PORT}/metrics', @@ -80,11 +83,22 @@ def dd_environment(): for controller in CONTROLLERS ] - metadata = {'agent_type': 'kubernetes', 'kubernetes': {'kubeconfig': kubeconfig}} + metadata = { + 'agent_type': 'kubernetes', + 'kubernetes': { + 'kubeconfig': kubeconfig, + 'auto_conf': os.path.join(CHECK_ROOT, 'datadog_checks', 'fluxcd', 'data', 'auto_conf.yaml'), + }, + } yield {'instances': instances}, metadata +@pytest.fixture(scope='session') +def fluxcd_kubeconfig(): + return get_state(KUBECONFIG_STATE) + + @pytest.fixture def instance(): return { diff --git a/fluxcd/tests/test_e2e.py b/fluxcd/tests/test_e2e.py index dc5742a58468e..c9931a981eb4b 100644 --- a/fluxcd/tests/test_e2e.py +++ b/fluxcd/tests/test_e2e.py @@ -2,18 +2,30 @@ # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) +import pytest + +from datadog_checks.base.stubs.aggregator import AggregatorStub +from datadog_checks.dev.kubernetes import assert_all_discovery_candidates_stable_kubernetes from datadog_checks.dev.utils import get_metadata_metrics +from datadog_checks.fluxcd import FluxcdCheck from .common import EXPECTED_METRICS +# All flux-system controllers deployed by the kind fixture and matched by the check's +# ad_identifiers, including image-automation-controller, which the non-discovery E2E test's +# fixed instance list omits. image-reflector-controller is also a valid ad_identifier but isn't +# listed here: the kind fixture's install.yaml deliberately excludes its Deployment because it +# never reached Ready in CI, so it has no running pod for the discovery E2E tests to exercise. +ALL_CONTROLLERS = ( + 'source-controller', + 'helm-controller', + 'image-automation-controller', + 'kustomize-controller', + 'notification-controller', +) -def test_source_controller_metrics(dd_agent_check): - """ - This only tests version 2 of flux. - Version 1 is in maintenance mode, all our users are on version 2. - """ - aggregator = dd_agent_check() +def assert_metrics(aggregator: AggregatorStub) -> None: ignore = { 'fluxcd.controller.runtime.reconcile.count', 'fluxcd.controller.runtime.reconcile.errors.count', @@ -35,3 +47,33 @@ def test_source_controller_metrics(dd_agent_check): aggregator.assert_metric(metric_name) aggregator.assert_all_metrics_covered() aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_source_controller_metrics(dd_agent_check): + """ + This only tests version 2 of flux. + + Version 1 is in maintenance mode, all our users are on version 2. + """ + aggregator = dd_agent_check() + assert_metrics(aggregator) + + +@pytest.mark.e2e +def test_e2e_discovery(dd_agent_check_discovery): + # Kubelet Autodiscovery is expected to find all five flux-system controller pods (the four + # exercised by the non-discovery E2E test above plus image-automation-controller). + aggregator = dd_agent_check_discovery(discovery_min_instances=len(ALL_CONTROLLERS)) + assert_metrics(aggregator) + + +@pytest.mark.e2e +@pytest.mark.parametrize('controller', ALL_CONTROLLERS) +def test_e2e_discovery_all_candidates(dd_agent_check, fluxcd_kubeconfig, controller): + assert_all_discovery_candidates_stable_kubernetes( + dd_agent_check, + FluxcdCheck, + fluxcd_kubeconfig, + namespace='flux-system', + pod_selector=f'app={controller}', + )