Skip to content

Commit 861bd5a

Browse files
authored
Add Kubernetes AgentInterface E2E backend (#24639)
* Add Kubernetes Agent E2E backend * Use draft PR number for changelogs * Remove unused Velero Kind port mapping * Use shared named-port discovery helper * Test generated discovery behavior * Test generated discovery through public contract * Extract Velero migration into stacked PR * Encapsulate Agent backend startup policy * Add Kubernetes discovery candidate stability helper * Document Kubernetes candidate stability support * Clean up unsupported CI environments * Clarify Agent interface creation * refactor(e2e): move Kubernetes discovery helper to consumer PR * refactor(e2e): avoid caching Kubernetes Agent properties * Restore unsupported CI backend state handling * Clarify Kubernetes Agent constraints * refactor(ddev): use singular Agent build config keys * Remove Kubernetes Agent pod label option * Remove custom Kubernetes Agent namespace option * Deduplicate Agent log commands * Simplify Kubernetes metadata access * refactor(ddev): remove Kubernetes Agent owner ID * Simplify Kubernetes Agent cluster ownership * Update logs tests to Python 3.13 * Refine Kubernetes command contract tests * Test Kubernetes topology command directly * Reject non-Kind Kubernetes Agent contexts * Relax Kubernetes orchestration assertions * Update Kubernetes Agent description for E2E tests * Simplify Kubernetes local package metadata * Restore environment state cleanup ordering * Wait for Kubernetes Agent before restart * Fail fast on unsupported Agent supervision * Simplify Kubernetes Agent restart checks * Keep Kubernetes Agent restarts repeatable * Detect Kubernetes Agent container state loss * Rename Kubernetes Agent module constants * Update Kubernetes node support description Clarified the implementation details regarding Kubernetes node support. * Decouple Kubernetes Agent tests from commands * Expose shared Agent image normalization * Require a Kubernetes metadata mapping Reading self.metadata['kubernetes'] unguarded turned a missing metadata block into a bare KeyError, so an environment that selects agent_type 'kubernetes' without the accompanying mapping failed with 'Unable to start the Agent: kubernetes' and gave a raw traceback from ddev env shell, which only handles CalledProcessError. Validate the mapping where it is read so the failure names the missing contract instead. * Stamp the prepared marker before restarting the Agent start() stamped PREPARED_MARKER after _restart_agent_process(), so a container replaced during the restart was marked prepared. The marker lives in the container filesystem and the pod declares no volumes, so a replacement loses the copied conf.yaml, auto_conf.yaml and the editable installs, while _wait_for_agent() still succeeds against the fresh container. Every later _require_prepared() check then passed and tests silently exercised the Agent-shipped integration with no configuration. Stamp the marker once preparation is complete and assert it after the restart, matching the invariant restart() already relies on. Checking after the stamp instead would only observe the gap between the two commands. * Share Agent image normalization through the interface * Clarify the prepared marker invariant * Move Agent image normalization to a shared module * Fail fast on unsupported Kubernetes Agent inputs * Clarify shared-log backend limitation * Preserve Agent check failures during config cleanup * Clarify current Kubernetes Agent limitations * Keep kubectl warnings out of structured output
1 parent e681f1e commit 861bd5a

24 files changed

Lines changed: 1416 additions & 80 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add backend-neutral Agent log retrieval for E2E test diagnostics.

datadog_checks_dev/datadog_checks/dev/plugin/pytest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,10 @@ def run_check(config=None, **kwargs):
219219

220220
if not matches:
221221
message_parts = []
222-
debug_result = run_command(['docker', 'logs', 'dd_{}_{}'.format(check, env)], capture=True)
222+
debug_result = run_command(
223+
[python_path, '-m', 'ddev', 'env', 'logs', check, env],
224+
capture=True,
225+
)
223226
if not debug_result.code:
224227
message_parts.append(debug_result.stdout + debug_result.stderr)
225228

ddev/changelog.d/24639.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a Kubernetes Agent interface for running the Agent in Kind-based E2E test clusters, alongside the existing Docker and Vagrant interfaces.

ddev/src/ddev/cli/env/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ddev.cli.env.agent import agent
77
from ddev.cli.env.check import check
88
from ddev.cli.env.config import config
9+
from ddev.cli.env.logs import logs
910
from ddev.cli.env.reload import reload_command
1011
from ddev.cli.env.shell import shell
1112
from ddev.cli.env.show import show
@@ -24,6 +25,7 @@ def env():
2425
env.add_command(agent)
2526
env.add_command(check)
2627
env.add_command(config)
28+
env.add_command(logs)
2729
env.add_command(reload_command)
2830
env.add_command(shell)
2931
env.add_command(show)

ddev/src/ddev/cli/env/agent.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ def _validate_env_vars(ctx: click.Context, param: click.Parameter, value: tuple[
4949
return env_vars or None
5050

5151

52+
def _sync_restored_config(app: Application, agent: AgentInterface) -> None:
53+
import sys
54+
55+
original_error = sys.exception()
56+
try:
57+
agent.sync_config()
58+
except Exception as e:
59+
if original_error is None:
60+
raise
61+
app.display_warning(f'Unable to restore the Agent configuration: {e}')
62+
63+
5264
@click.command(
5365
short_help='Invoke the Agent', context_settings={'help_option_names': [], 'ignore_unknown_options': True}
5466
)
@@ -79,9 +91,8 @@ def agent(
7991
"""
8092
import subprocess
8193

82-
from ddev.e2e.agent import get_agent_interface
94+
from ddev.e2e.agent import create_agent_interface
8395
from ddev.e2e.config import EnvDataStorage
84-
from ddev.e2e.constants import DEFAULT_AGENT_TYPE, E2EMetadata
8596
from ddev.utils.fs import Path
8697

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

93104
metadata = env_data.read_metadata()
94-
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
95-
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
105+
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)
96106

97107
full_args = list(args)
98108
trigger_run = False
@@ -131,6 +141,7 @@ def agent(
131141
app.abort(str(e))
132142
finally:
133143
env_data.config_file.unlink()
144+
_sync_restored_config(app, agent)
134145
else:
135146
temp_config_file = env_data.config_file.parent / f'{env_data.config_file.name}.bak.example'
136147
env_data.config_file.replace(temp_config_file)
@@ -141,3 +152,4 @@ def agent(
141152
app.abort(str(e))
142153
finally:
143154
temp_config_file.replace(env_data.config_file)
155+
_sync_restored_config(app, agent)

ddev/src/ddev/cli/env/logs.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# (C) Datadog, Inc. 2026-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from __future__ import annotations
5+
6+
from typing import TYPE_CHECKING
7+
8+
import click
9+
10+
if TYPE_CHECKING:
11+
from ddev.cli.application import Application
12+
13+
14+
@click.command('logs', short_help='Show logs for the Agent')
15+
@click.argument('intg_name', metavar='INTEGRATION')
16+
@click.argument('environment')
17+
@click.pass_obj
18+
def logs(app: Application, *, intg_name: str, environment: str):
19+
"""Show backend-specific diagnostics for the Agent."""
20+
from ddev.e2e.agent import create_agent_interface
21+
from ddev.e2e.config import EnvDataStorage
22+
23+
integration = app.repo.integrations.get(intg_name)
24+
env_data = EnvDataStorage(app.data_dir).get(integration.name, environment)
25+
26+
if not env_data.exists():
27+
app.abort(f'Environment `{environment}` for integration `{integration.name}` is not running')
28+
29+
metadata = env_data.read_metadata()
30+
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)
31+
32+
try:
33+
agent.show_logs()
34+
except Exception as e:
35+
app.abort(str(e))

ddev/src/ddev/cli/env/reload.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ def reload_command(app: Application, *, intg_name: str, environment: str):
1919
"""
2020
Restart the Agent to detect environment changes.
2121
"""
22-
from ddev.e2e.agent import get_agent_interface
22+
from ddev.e2e.agent import create_agent_interface
2323
from ddev.e2e.config import EnvDataStorage
24-
from ddev.e2e.constants import DEFAULT_AGENT_TYPE, E2EMetadata
2524

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

3231
metadata = env_data.read_metadata()
33-
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
34-
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
32+
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)
3533

3634
try:
3735
agent.restart()

ddev/src/ddev/cli/env/shell.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ def shell(app: Application, *, intg_name: str, environment: str):
2121
"""
2222
import subprocess
2323

24-
from ddev.e2e.agent import get_agent_interface
24+
from ddev.e2e.agent import create_agent_interface
2525
from ddev.e2e.config import EnvDataStorage
26-
from ddev.e2e.constants import DEFAULT_AGENT_TYPE, E2EMetadata
2726

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

3433
metadata = env_data.read_metadata()
35-
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
36-
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
34+
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)
3735

3836
try:
3937
agent.enter_shell()

ddev/src/ddev/cli/env/show.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def show(app: Application, *, intg_name: str | None, environment: str | None, fo
7272
app.display_table('Available', available_columns, show_lines=True, force_ascii=force_ascii)
7373
# Display information about a specific environment
7474
else:
75-
from ddev.e2e.agent import get_agent_interface
75+
from ddev.e2e.agent import create_agent_interface
7676

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

8383
metadata = env_data.read_metadata()
8484
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
85-
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
85+
agent = create_agent_interface(app, integration, environment, metadata, env_data.config_file)
8686

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

ddev/src/ddev/cli/env/start.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def start(
132132
result = json.loads(result_file.read_text())
133133

134134
metadata = result['metadata']
135+
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
135136

136137
# TODO Remove once we have migrated the `docker_run` function
137138
if serialized_volumes := metadata.get(E2EMetadata.ENV_VARS, {}).get(E2EEnvVars.DOCKER_VOLUMES):
@@ -144,24 +145,26 @@ def start(
144145
config = result['config']
145146
env_data.write_config(config)
146147

147-
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
148-
149-
if agent_type == "vagrant" and running_on_ci():
150-
app.abort(text="Vagrant is not supported on CI", code=0)
148+
agent_class = get_agent_interface(agent_type)
149+
if running_on_ci() and not agent_class.supports_ci:
150+
app.abort(text=f'{agent_type.capitalize()} is not supported on CI', code=0)
151151

152-
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
152+
agent = agent_class(app, integration, environment, metadata, env_data.config_file)
153153

154154
if not agent_build:
155+
configured_agent_build = agent.get_configured_build(app.config.agent.config)
155156
agent_build = (
156157
os.getenv(E2EEnvVars.AGENT_BUILD_PY2 if agent.python_version[0] == 2 else E2EEnvVars.AGENT_BUILD)
157-
or app.config.agent.config.get(agent_type)
158+
or configured_agent_build
158159
or ''
159160
)
160161

161162
agent_env_vars = _get_agent_env_vars(app.config.org.config, metadata, extra_env_vars, dogstatsd)
162163

163164
try:
164165
agent.start(agent_build=agent_build, local_packages=local_packages, env_vars=agent_env_vars)
166+
# Backends may add runtime metadata needed by later ddev processes.
167+
env_data.write_metadata(metadata)
165168
except Exception as e:
166169
from ddev.cli.env.stop import stop
167170

0 commit comments

Comments
 (0)