Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
6b61771
Add Kubernetes Agent E2E backend
nubtron Jul 22, 2026
496b3c2
Use draft PR number for changelogs
nubtron Jul 22, 2026
d4e2ba3
Remove unused Velero Kind port mapping
nubtron Jul 22, 2026
0ae3476
Use shared named-port discovery helper
nubtron Jul 22, 2026
9711d66
Test generated discovery behavior
nubtron Jul 22, 2026
0065786
Test generated discovery through public contract
nubtron Jul 22, 2026
9f63a54
Extract Velero migration into stacked PR
nubtron Jul 22, 2026
23d1947
Encapsulate Agent backend startup policy
nubtron Jul 22, 2026
04d2bc5
Add Kubernetes discovery candidate stability helper
nubtron Jul 22, 2026
fb00f3e
Document Kubernetes candidate stability support
nubtron Jul 22, 2026
154ce6a
Clean up unsupported CI environments
nubtron Jul 23, 2026
9b88ee8
Clarify Agent interface creation
nubtron Jul 23, 2026
00f16dd
refactor(e2e): move Kubernetes discovery helper to consumer PR
nubtron Jul 23, 2026
64708c7
refactor(e2e): avoid caching Kubernetes Agent properties
nubtron Jul 23, 2026
5be4a41
Restore unsupported CI backend state handling
nubtron Jul 23, 2026
9135c28
Clarify Kubernetes Agent constraints
nubtron Jul 23, 2026
4377d08
refactor(ddev): use singular Agent build config keys
nubtron Jul 23, 2026
25cf633
Remove Kubernetes Agent pod label option
nubtron Jul 23, 2026
3fc1fe2
Remove custom Kubernetes Agent namespace option
nubtron Jul 23, 2026
888d35d
Deduplicate Agent log commands
nubtron Jul 23, 2026
63bc206
Simplify Kubernetes metadata access
nubtron Jul 23, 2026
bd97f8f
refactor(ddev): remove Kubernetes Agent owner ID
nubtron Jul 23, 2026
7b9533c
Simplify Kubernetes Agent cluster ownership
nubtron Jul 23, 2026
4bcd7de
Update logs tests to Python 3.13
nubtron Jul 23, 2026
6dd5a08
Refine Kubernetes command contract tests
nubtron Jul 23, 2026
9373d27
Test Kubernetes topology command directly
nubtron Jul 23, 2026
214fe28
Reject non-Kind Kubernetes Agent contexts
nubtron Jul 23, 2026
9c882b9
Relax Kubernetes orchestration assertions
nubtron Jul 23, 2026
1375924
Update Kubernetes Agent description for E2E tests
nubtron Jul 24, 2026
e0f8dc8
Simplify Kubernetes local package metadata
nubtron Jul 24, 2026
aedf070
Merge remote-tracking branch 'origin/nubtron/kubernetes-agent-interfa…
nubtron Jul 24, 2026
30d4ae7
Restore environment state cleanup ordering
nubtron Jul 24, 2026
3577534
Wait for Kubernetes Agent before restart
nubtron Jul 24, 2026
4cc88c7
Fail fast on unsupported Agent supervision
nubtron Jul 24, 2026
3ea3f6d
Simplify Kubernetes Agent restart checks
nubtron Jul 24, 2026
827abf9
Keep Kubernetes Agent restarts repeatable
nubtron Jul 24, 2026
486423f
Detect Kubernetes Agent container state loss
nubtron Jul 24, 2026
71fa112
Rename Kubernetes Agent module constants
nubtron Jul 24, 2026
4378e5f
Update Kubernetes node support description
nubtron Jul 24, 2026
cb59144
Decouple Kubernetes Agent tests from commands
nubtron Jul 24, 2026
5f720f5
Expose shared Agent image normalization
nubtron Jul 24, 2026
5ba8c48
Require a Kubernetes metadata mapping
nubtron Jul 30, 2026
01d184a
Stamp the prepared marker before restarting the Agent
nubtron Jul 30, 2026
f1aae67
Share Agent image normalization through the interface
nubtron Jul 30, 2026
52463b4
Clarify the prepared marker invariant
nubtron Jul 30, 2026
183bcfb
Merge branch 'master' into nubtron/kubernetes-agent-interface-e2e
nubtron Jul 30, 2026
2dfc564
Move Agent image normalization to a shared module
nubtron Jul 30, 2026
c8cc16b
Run Argo Rollouts E2E with Kubernetes Agent
nubtron Jul 23, 2026
17dd895
Run Kyverno E2E with Kubernetes Agent
nubtron Jul 23, 2026
a8e3c68
Run Argo CD E2E with Kubernetes Agent
nubtron Jul 23, 2026
001f0c4
Run Tekton E2E with Kubernetes Agent
nubtron Jul 23, 2026
4fbc615
Run KEDA E2E with Kubernetes Agent
nubtron Jul 23, 2026
6bc1e7c
Run Calico E2E with Kubernetes Agent
nubtron Jul 23, 2026
7d2df81
Run Kuma E2E with Kubernetes Agent
nubtron Jul 22, 2026
a9cf8ae
Log resolved Kuma module source
nubtron Jul 24, 2026
666ec3d
Merge exact Kuma consumer branch into validation suite
nubtron Jul 30, 2026
a151685
Merge exact Argo CD consumer branch into validation suite
nubtron Jul 30, 2026
5b080bf
Merge exact KEDA consumer branch into validation suite
nubtron Jul 30, 2026
7861b65
Merge exact Kyverno consumer branch into validation suite
nubtron Jul 30, 2026
2b5d507
Merge exact Calico consumer branch into validation suite
nubtron Jul 30, 2026
c927c93
Merge exact Argo Rollouts consumer branch into validation suite
nubtron Jul 30, 2026
d9c7b38
Merge exact Tekton consumer branch into validation suite
nubtron Jul 30, 2026
e5a8b40
Use aggregate PR number for changelogs
nubtron Jul 30, 2026
9bc4710
Fail fast on unsupported Kubernetes Agent inputs
nubtron Aug 3, 2026
8932f13
Preserve Agent check failures during config cleanup
nubtron Aug 3, 2026
d1c695d
Keep kubectl warnings out of structured output
nubtron Aug 3, 2026
2151a50
Clarify shared-log backend limitation
nubtron Aug 3, 2026
0e1697a
Merge unsupported-options documentation update
nubtron Aug 3, 2026
4587ae2
Merge config-cleanup stack update
nubtron Aug 3, 2026
72ecf4c
Clarify current Kubernetes Agent limitations
nubtron Aug 3, 2026
e7f5199
Merge unsupported-options wording update
nubtron Aug 3, 2026
2a64043
Merge config-cleanup stack update
nubtron Aug 3, 2026
d945435
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
5f6000f
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
cc0cdc6
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
c922643
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
34d6c3b
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
3c98d27
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
d863f44
Merge updated Kubernetes Agent backend
nubtron Aug 3, 2026
377ce51
Merge exact Kubernetes Agent backend into validation suite
nubtron Aug 3, 2026
3abdb80
Merge exact Kuma consumer branch into validation suite
nubtron Aug 3, 2026
6cb2629
Merge exact Argo CD consumer branch into validation suite
nubtron Aug 3, 2026
8ecc71a
Merge exact KEDA consumer branch into validation suite
nubtron Aug 3, 2026
83d04d3
Merge exact Kyverno consumer branch into validation suite
nubtron Aug 3, 2026
6c755f1
Merge exact Calico consumer branch into validation suite
nubtron Aug 3, 2026
8c9574e
Merge exact Argo Rollouts consumer branch into validation suite
nubtron Aug 3, 2026
16919ea
Merge exact Tekton consumer branch into validation suite
nubtron Aug 3, 2026
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
15 changes: 7 additions & 8 deletions argo_rollouts/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import os
from contextlib import ExitStack

import pytest

from datadog_checks.dev import get_here
from datadog_checks.dev.kind import kind_run
from datadog_checks.dev.kube_port_forward import port_forward
from datadog_checks.dev.subprocess import run_command

HERE = get_here()
Expand All @@ -27,10 +25,11 @@ def setup_argo_rollouts():

@pytest.fixture(scope='session')
def dd_environment():
with kind_run(conditions=[setup_argo_rollouts], sleep=30) as kubeconfig, ExitStack() as stack:
argo_rollouts_host, argo_rollouts_port = stack.enter_context(
port_forward(kubeconfig, 'argo-rollouts', 8090, 'deployment', 'argo-rollouts')
)
instances = [{'openmetrics_endpoint': f'http://{argo_rollouts_host}:{argo_rollouts_port}/metrics'}]
with kind_run(conditions=[setup_argo_rollouts], sleep=30) as kubeconfig:
instances = [
{'openmetrics_endpoint': 'http://argo-rollouts-metrics.argo-rollouts.svc.cluster.local:8090/metrics'}
]

yield {'instances': instances}
metadata = {'agent_type': 'kubernetes', 'kubernetes': {'kubeconfig': kubeconfig}}

yield {'instances': instances}, metadata
65 changes: 22 additions & 43 deletions argocd/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,8 @@

from datadog_checks.dev import get_here
from datadog_checks.dev.kind import kind_run
from datadog_checks.dev.kube_port_forward import port_forward
from datadog_checks.dev.subprocess import run_command

try:
from contextlib import ExitStack
except ImportError:
from contextlib2 import ExitStack


HERE = get_here()
opj = os.path.join

Expand All @@ -40,39 +33,25 @@ def setup_argocd():
@pytest.fixture(scope='session')
def dd_environment(dd_save_state):
with kind_run(conditions=[setup_argocd]) as kubeconfig:
with ExitStack() as stack:
app_controller_host, app_controller_port = stack.enter_context(
port_forward(kubeconfig, 'argocd', 8082, 'service', 'argocd-metrics')
)
appset_controller_host, appset_controller_port = stack.enter_context(
port_forward(kubeconfig, 'argocd', 8080, 'service', 'argocd-applicationset-controller')
)
api_server_host, api_server_port = stack.enter_context(
port_forward(kubeconfig, 'argocd', 8083, 'service', 'argocd-server-metrics')
)
repo_server_host, repo_server_port = stack.enter_context(
port_forward(kubeconfig, 'argocd', 8084, 'service', 'argocd-repo-server')
)
notifications_controller_host, notifications_controller_port = stack.enter_context(
port_forward(kubeconfig, 'argocd', 9001, 'service', 'argocd-notifications-controller-metrics')
)
app_controller_endpoint = 'http://{}:{}/metrics'.format(app_controller_host, app_controller_port)
appset_controller_endpoint = 'http://{}:{}/metrics'.format(appset_controller_host, appset_controller_port)
api_server_endpoint = 'http://{}:{}/metrics'.format(api_server_host, api_server_port)
repo_server_endpoint = 'http://{}:{}/metrics'.format(repo_server_host, repo_server_port)
notifications_controller_endpoint = 'http://{}:{}/metrics'.format(
notifications_controller_host, notifications_controller_port
)

instance = {
'app_controller_endpoint': app_controller_endpoint,
'appset_controller_endpoint': appset_controller_endpoint,
'api_server_endpoint': api_server_endpoint,
'repo_server_endpoint': repo_server_endpoint,
'notifications_controller_endpoint': notifications_controller_endpoint,
}

# save this instance to use for openmetrics_v2 instance, since the endpoint is different each run
dd_save_state("argocd_instance", instance)

yield instance
instance = {
'app_controller_endpoint': 'http://argocd-metrics.argocd.svc.cluster.local:8082/metrics',
'appset_controller_endpoint': (
'http://argocd-applicationset-controller.argocd.svc.cluster.local:8080/metrics'
),
'api_server_endpoint': 'http://argocd-server-metrics.argocd.svc.cluster.local:8083/metrics',
'repo_server_endpoint': 'http://argocd-repo-server.argocd.svc.cluster.local:8084/metrics',
'notifications_controller_endpoint': (
'http://argocd-notifications-controller-metrics.argocd.svc.cluster.local:9001/metrics'
),
}
metadata = {
'agent_type': 'kubernetes',
'kubernetes': {
'kubeconfig': kubeconfig,
},
}

# Save this instance to use for the openmetrics_v2 instance.
dd_save_state("argocd_instance", instance)

yield instance, metadata
59 changes: 40 additions & 19 deletions calico/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,49 @@

import pytest

from datadog_checks.dev.conditions import CheckEndpoints, WaitFor
from datadog_checks.dev.conditions import WaitFor
from datadog_checks.dev.kind import kind_run
from datadog_checks.dev.kube_port_forward import port_forward
from datadog_checks.dev.subprocess import run_command

from .common import EXTRA_METRICS

NAMESPACE = "calico"
HERE = path.dirname(path.abspath(__file__))
FELIX_METRICS_ENDPOINT = 'http://felix-metrics-svc.kube-system.svc.cluster.local:9091/metrics'


def _felix_config_default_exists():
result = run_command(["kubectl", "get", "felixconfiguration", "default"], capture='both')
return result.code == 0


def felix_metrics_available() -> bool:
result = run_command(
[
"kubectl",
"run",
"felix-metrics-readiness",
"--namespace",
"kube-system",
"--image=busybox:1.36.1",
"--restart=Never",
"--attach",
"--rm",
"--quiet",
"--",
"wget",
"-q",
"-T",
"2",
"-O",
"/dev/null",
FELIX_METRICS_ENDPOINT,
],
capture='both',
)
return result.code == 0


def setup_calico():
# Deploy calico
run_command(["kubectl", "apply", "-f", path.join(HERE, 'kind', 'calico.yaml')])
Expand Down Expand Up @@ -60,29 +87,23 @@ def setup_calico():
check=True,
)

# Check from a temporary pod because the host cannot resolve Kubernetes Service DNS.
WaitFor(felix_metrics_available, wait=2, attempts=100)()


@pytest.fixture(scope='session')
def dd_environment():
with (
kind_run(
conditions=[setup_calico], kind_config=path.join(HERE, 'kind', 'kind-calico.yaml'), sleep=10
) as kubeconfig,
port_forward(kubeconfig, 'kube-system', 9091, 'service', 'felix-metrics-svc') as (
calico_host,
calico_port,
),
):
endpoint = 'http://{}:{}/metrics'.format(calico_host, calico_port)

# We can't add this to `kind_run` because we don't know the URL at this moment
condition = CheckEndpoints(endpoint, wait=2, attempts=100)
condition()

yield {
"openmetrics_endpoint": endpoint,
with kind_run(
conditions=[setup_calico], kind_config=path.join(HERE, 'kind', 'kind-calico.yaml'), sleep=10
) as kubeconfig:
instance = {
"openmetrics_endpoint": FELIX_METRICS_ENDPOINT,
"namespace": NAMESPACE,
"extra_metrics": EXTRA_METRICS,
}
metadata = {'agent_type': 'kubernetes', 'kubernetes': {'kubeconfig': kubeconfig}}

yield instance, metadata


@pytest.fixture
Expand Down
1 change: 1 addition & 0 deletions datadog_checks_dev/changelog.d/24680.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add backend-neutral Agent log retrieval for E2E test diagnostics.
5 changes: 4 additions & 1 deletion datadog_checks_dev/datadog_checks/dev/plugin/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,10 @@ def run_check(config=None, **kwargs):

if not matches:
message_parts = []
debug_result = run_command(['docker', 'logs', 'dd_{}_{}'.format(check, env)], capture=True)
debug_result = run_command(
[python_path, '-m', 'ddev', 'env', 'logs', check, env],
capture=True,
)
if not debug_result.code:
message_parts.append(debug_result.stdout + debug_result.stderr)

Expand Down
1 change: 1 addition & 0 deletions ddev/changelog.d/24680.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a Kubernetes Agent interface for running the Agent in Kind-based E2E test clusters, alongside the existing Docker and Vagrant interfaces.
2 changes: 2 additions & 0 deletions ddev/src/ddev/cli/env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ddev.cli.env.agent import agent
from ddev.cli.env.check import check
from ddev.cli.env.config import config
from ddev.cli.env.logs import logs
from ddev.cli.env.reload import reload_command
from ddev.cli.env.shell import shell
from ddev.cli.env.show import show
Expand All @@ -24,6 +25,7 @@ def env():
env.add_command(agent)
env.add_command(check)
env.add_command(config)
env.add_command(logs)
env.add_command(reload_command)
env.add_command(shell)
env.add_command(show)
Expand Down
20 changes: 16 additions & 4 deletions ddev/src/ddev/cli/env/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ def _validate_env_vars(ctx: click.Context, param: click.Parameter, value: tuple[
return env_vars or None


def _sync_restored_config(app: Application, agent: AgentInterface) -> None:
import sys

original_error = sys.exception()
try:
agent.sync_config()
except Exception as e:
if original_error is None:
raise
app.display_warning(f'Unable to restore the Agent configuration: {e}')


@click.command(
short_help='Invoke the Agent', context_settings={'help_option_names': [], 'ignore_unknown_options': True}
)
Expand Down Expand Up @@ -79,9 +91,8 @@ def agent(
"""
import subprocess

from ddev.e2e.agent import get_agent_interface
from ddev.e2e.agent import create_agent_interface
from ddev.e2e.config import EnvDataStorage
from ddev.e2e.constants import DEFAULT_AGENT_TYPE, E2EMetadata
from ddev.utils.fs import Path

integration = app.repo.integrations.get(intg_name)
Expand All @@ -91,8 +102,7 @@ def agent(
app.abort(f'Environment `{environment}` for integration `{integration.name}` is not running')

metadata = env_data.read_metadata()
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)

full_args = list(args)
trigger_run = False
Expand Down Expand Up @@ -131,6 +141,7 @@ def agent(
app.abort(str(e))
finally:
env_data.config_file.unlink()
_sync_restored_config(app, agent)
else:
temp_config_file = env_data.config_file.parent / f'{env_data.config_file.name}.bak.example'
env_data.config_file.replace(temp_config_file)
Expand All @@ -141,3 +152,4 @@ def agent(
app.abort(str(e))
finally:
temp_config_file.replace(env_data.config_file)
_sync_restored_config(app, agent)
35 changes: 35 additions & 0 deletions ddev/src/ddev/cli/env/logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
from __future__ import annotations

from typing import TYPE_CHECKING

import click

if TYPE_CHECKING:
from ddev.cli.application import Application


@click.command('logs', short_help='Show logs for the Agent')
@click.argument('intg_name', metavar='INTEGRATION')
@click.argument('environment')
@click.pass_obj
def logs(app: Application, *, intg_name: str, environment: str):
"""Show backend-specific diagnostics for the Agent."""
from ddev.e2e.agent import create_agent_interface
from ddev.e2e.config import EnvDataStorage

integration = app.repo.integrations.get(intg_name)
env_data = EnvDataStorage(app.data_dir).get(integration.name, environment)

if not env_data.exists():
app.abort(f'Environment `{environment}` for integration `{integration.name}` is not running')

metadata = env_data.read_metadata()
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)

try:
agent.show_logs()
except Exception as e:
app.abort(str(e))
6 changes: 2 additions & 4 deletions ddev/src/ddev/cli/env/reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ def reload_command(app: Application, *, intg_name: str, environment: str):
"""
Restart the Agent to detect environment changes.
"""
from ddev.e2e.agent import get_agent_interface
from ddev.e2e.agent import create_agent_interface
from ddev.e2e.config import EnvDataStorage
from ddev.e2e.constants import DEFAULT_AGENT_TYPE, E2EMetadata

integration = app.repo.integrations.get(intg_name)
env_data = EnvDataStorage(app.data_dir).get(integration.name, environment)
Expand All @@ -30,8 +29,7 @@ def reload_command(app: Application, *, intg_name: str, environment: str):
app.abort(f'Environment `{environment}` for integration `{integration.name}` is not running')

metadata = env_data.read_metadata()
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)

try:
agent.restart()
Expand Down
6 changes: 2 additions & 4 deletions ddev/src/ddev/cli/env/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ def shell(app: Application, *, intg_name: str, environment: str):
"""
import subprocess

from ddev.e2e.agent import get_agent_interface
from ddev.e2e.agent import create_agent_interface
from ddev.e2e.config import EnvDataStorage
from ddev.e2e.constants import DEFAULT_AGENT_TYPE, E2EMetadata

integration = app.repo.integrations.get(intg_name)
env_data = EnvDataStorage(app.data_dir).get(integration.name, environment)
Expand All @@ -32,8 +31,7 @@ def shell(app: Application, *, intg_name: str, environment: str):
app.abort(f'Environment `{environment}` for integration `{integration.name}` is not running')

metadata = env_data.read_metadata()
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)

try:
agent.enter_shell()
Expand Down
4 changes: 2 additions & 2 deletions ddev/src/ddev/cli/env/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def show(app: Application, *, intg_name: str | None, environment: str | None, fo
app.display_table('Available', available_columns, show_lines=True, force_ascii=force_ascii)
# Display information about a specific environment
else:
from ddev.e2e.agent import get_agent_interface
from ddev.e2e.agent import create_agent_interface

integration = app.repo.integrations.get(intg_name)
env_data = storage.get(integration.name, environment)
Expand All @@ -82,7 +82,7 @@ def show(app: Application, *, intg_name: str | None, environment: str | None, fo

metadata = env_data.read_metadata()
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)

app.display_pair('Agent type', agent_type)
app.display_pair('Agent ID', agent.get_id())
Loading
Loading