Skip to content
Open
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
25 changes: 25 additions & 0 deletions fluxcd/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Comment thread
vitkyrka marked this conversation as resolved.
- 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
1 change: 1 addition & 0 deletions fluxcd/changelog.d/24510.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add container-based config discovery support.
42 changes: 42 additions & 0 deletions fluxcd/datadog_checks/fluxcd/config_models/discovery.py
Original file line number Diff line number Diff line change
@@ -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 <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, 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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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:<function_name>`. 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': ...}
30 changes: 30 additions & 0 deletions fluxcd/datadog_checks/fluxcd/data/auto_conf.yaml
Original file line number Diff line number Diff line change
@@ -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: []
2 changes: 1 addition & 1 deletion fluxcd/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
"Private :: Do Not Upload",
]
dependencies = [
"datadog-checks-base>=37.33.0",
"datadog-checks-base>=38.0.0",
]
dynamic = [
"version",
Expand Down
16 changes: 15 additions & 1 deletion fluxcd/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -73,18 +75,30 @@ 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',
}
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 {
Expand Down
54 changes: 48 additions & 6 deletions fluxcd/tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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}',
)
Loading