Skip to content

Commit b585bc6

Browse files
vitkyrkaclaude
andcommitted
Support per-invocation env vars for e2e Agent containers
and a new --env option on ddev env agent check to set an environment variable scoped to a single agent check invocation (via docker exec -e) rather than the whole Agent container. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 28830fd commit b585bc6

6 files changed

Lines changed: 47 additions & 17 deletions

File tree

ddev/changelog.d/24289.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for the `--env` option to `ddev env agent` to pass environment variables.

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,21 @@
1212
from ddev.e2e.agent.interface import AgentInterface
1313

1414

15-
def _invoke_check_with_retry(agent: AgentInterface, args: list[str], *, retries: int = 3, backoff: float = 0.5) -> None:
15+
def _invoke_check_with_retry(
16+
agent: AgentInterface,
17+
args: list[str],
18+
*,
19+
env_vars: dict[str, str] | None = None,
20+
retries: int = 3,
21+
backoff: float = 0.5,
22+
) -> None:
1623
"""Invoke ``agent check`` with bounded retry to absorb transient autodiscovery-reload races."""
1724
import subprocess
1825
import time
1926

2027
for attempt in range(retries + 1):
2128
try:
22-
agent.invoke(args)
29+
agent.invoke(args, env_vars=env_vars)
2330
return
2431
except subprocess.CalledProcessError:
2532
if attempt >= retries:
@@ -38,8 +45,23 @@ def _invoke_check_with_retry(agent: AgentInterface, args: list[str], *, retries:
3845
@click.argument('environment')
3946
@click.argument('args', required=True, nargs=-1)
4047
@click.option('--config-file', hidden=True)
48+
@click.option(
49+
'--env',
50+
'env_vars',
51+
multiple=True,
52+
metavar='KEY=VALUE',
53+
help='Set an environment variable for this invocation only (may be repeated)',
54+
)
4155
@click.pass_obj
42-
def agent(app: Application, *, intg_name: str, environment: str, args: tuple[str, ...], config_file: str | None):
56+
def agent(
57+
app: Application,
58+
*,
59+
intg_name: str,
60+
environment: str,
61+
args: tuple[str, ...],
62+
config_file: str | None,
63+
env_vars: tuple[str, ...],
64+
):
4365
"""
4466
Invoke the Agent.
4567
"""
@@ -60,6 +82,8 @@ def agent(app: Application, *, intg_name: str, environment: str, args: tuple[str
6082
agent_type = metadata.get(E2EMetadata.AGENT_TYPE, DEFAULT_AGENT_TYPE)
6183
agent = get_agent_interface(agent_type)(app, integration, environment, metadata, env_data.config_file)
6284

85+
invoke_env_vars = dict(entry.split('=', 1) for entry in env_vars) if env_vars else None
86+
6387
full_args = list(args)
6488
trigger_run = False
6589
if full_args[0] == 'check':
@@ -75,9 +99,9 @@ def agent(app: Application, *, intg_name: str, environment: str, args: tuple[str
7599
if config_file is None or not trigger_run:
76100
try:
77101
if trigger_run:
78-
_invoke_check_with_retry(agent, full_args)
102+
_invoke_check_with_retry(agent, full_args, env_vars=invoke_env_vars)
79103
else:
80-
agent.invoke(full_args)
104+
agent.invoke(full_args, env_vars=invoke_env_vars)
81105
except subprocess.CalledProcessError as e:
82106
app.abort(code=e.returncode)
83107

@@ -90,14 +114,14 @@ def agent(app: Application, *, intg_name: str, environment: str, args: tuple[str
90114
if not env_data.config_file.is_file():
91115
try:
92116
env_data.write_config(config)
93-
_invoke_check_with_retry(agent, full_args)
117+
_invoke_check_with_retry(agent, full_args, env_vars=invoke_env_vars)
94118
finally:
95119
env_data.config_file.unlink()
96120
else:
97121
temp_config_file = env_data.config_file.parent / f'{env_data.config_file.name}.bak.example'
98122
env_data.config_file.replace(temp_config_file)
99123
try:
100124
env_data.write_config(config)
101-
_invoke_check_with_retry(agent, full_args)
125+
_invoke_check_with_retry(agent, full_args, env_vars=invoke_env_vars)
102126
finally:
103127
temp_config_file.replace(env_data.config_file)

ddev/src/ddev/e2e/agent/docker.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,12 @@ def _python_path(self) -> str:
121121
else f'/opt/datadog-agent/embedded/bin/python{self.python_version[0]}'
122122
)
123123

124-
def _format_command(self, command: list[str]) -> list[str]:
124+
def _format_command(self, command: list[str], *, env_vars: dict[str, str] | None = None) -> list[str]:
125125
cmd = ['docker', 'exec']
126126
if self._isatty:
127127
cmd.append('-it')
128+
for key, value in (env_vars or {}).items():
129+
cmd.extend(['-e', f'{key}={value}'])
128130
cmd.append(self._container_name)
129131

130132
if command[0] == 'pip':
@@ -364,11 +366,11 @@ def restart(self) -> None:
364366
f'Unable to restart Agent container `{self._container_name}`: {process.stdout.decode("utf-8")}'
365367
)
366368

367-
def invoke(self, args: list[str]) -> None:
368-
self.run_command(['agent', *args])
369+
def invoke(self, args: list[str], *, env_vars: dict[str, str] | None = None) -> None:
370+
self.run_command(['agent', *args], env_vars=env_vars)
369371

370-
def run_command(self, args: list[str]) -> None:
371-
self._run_command(self._format_command([*args]), check=True)
372+
def run_command(self, args: list[str], *, env_vars: dict[str, str] | None = None) -> None:
373+
self._run_command(self._format_command([*args], env_vars=env_vars), check=True)
372374

373375
def enter_shell(self) -> None:
374376
self._run_command(self._format_command(['cmd' if self._is_windows_container else 'bash']), check=True)

ddev/src/ddev/e2e/agent/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def stop(self) -> None: ...
7373
def restart(self) -> None: ...
7474

7575
@abstractmethod
76-
def invoke(self, args: list[str]) -> None: ...
76+
def invoke(self, args: list[str], *, env_vars: dict[str, str] | None = None) -> None: ...
7777

7878
@abstractmethod
7979
def enter_shell(self) -> None: ...

ddev/src/ddev/e2e/agent/vagrant.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ def restart(self) -> None:
116116
self._run_commands(guest_cmds, "restart_agent_service")
117117
self.app.display_info("Datadog Agent service restart sequence completed.")
118118

119-
def invoke(self, args: list[str]) -> None:
119+
def invoke(self, args: list[str], *, env_vars: dict[str, str] | None = None) -> None:
120+
if env_vars:
121+
raise NotImplementedError("Per-invocation env_vars are not supported for the Vagrant agent")
122+
120123
agent_bin = LINUX_AGENT_BIN_PATH if not self._is_windows_vm else WINDOWS_AGENT_BIN_PATH
121124
guest_cmd_parts = ["sudo", agent_bin] + args if not self._is_windows_vm else [agent_bin] + args
122125
host_cmd = self._format_command(guest_cmd_parts)

ddev/tests/cli/env/test_agent.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_not_trigger_run(ddev, data_dir, mocker):
3535
assert result.exit_code == 0, result.output
3636
assert not result.output
3737

38-
invoke.assert_called_once_with(['status'])
38+
invoke.assert_called_once_with(['status'], env_vars=None)
3939

4040

4141
def test_trigger_run(ddev, data_dir, mocker):
@@ -51,7 +51,7 @@ def test_trigger_run(ddev, data_dir, mocker):
5151
assert result.exit_code == 0, result.output
5252
assert not result.output
5353

54-
invoke.assert_called_once_with(['check', integration, '-l', 'debug'])
54+
invoke.assert_called_once_with(['check', integration, '-l', 'debug'], env_vars=None)
5555

5656

5757
def test_trigger_run_inject_integration(ddev, data_dir, mocker):
@@ -67,4 +67,4 @@ def test_trigger_run_inject_integration(ddev, data_dir, mocker):
6767
assert result.exit_code == 0, result.output
6868
assert not result.output
6969

70-
invoke.assert_called_once_with(['check', integration, '-l', 'debug'])
70+
invoke.assert_called_once_with(['check', integration, '-l', 'debug'], env_vars=None)

0 commit comments

Comments
 (0)