Skip to content

Commit 4ffc2cb

Browse files
authored
Merge branch 'main' into tosi/aadssh
2 parents 5259421 + 83e646e commit 4ffc2cb

File tree

17 files changed

+296
-41
lines changed

17 files changed

+296
-41
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,5 @@
337337
/src/storage-discovery/ @shanefujs @calvinhzy
338338

339339
/src/aks-agent/ @feiskyer @mainred @nilo19
340+
341+
/src/migrate/ @saifaldin14

src/aks-agent/HISTORY.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ To release a new version, please select a new version number (usually plus 1 to
1111

1212
Pending
1313
+++++++
14+
* Fix stdin reading hang in CI/CD pipelines by using select with timeout for non-interactive mode.
15+
* Update pytest marker registration and fix datetime.utcnow() deprecation warning in tests.
16+
* Improve test framework with real-time stderr output visibility and subprocess timeout.
1417

1518
1.0.0b7
1619
+++++++

src/aks-agent/azext_aks_agent/agent/agent.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import logging
77
import os
8+
import select
89
import sys
910

1011
from azext_aks_agent._consts import (
@@ -123,7 +124,7 @@ def _should_refresh_toolsets(requested_mode: str, user_refresh_request: bool) ->
123124
return False
124125

125126

126-
# pylint: disable=too-many-locals
127+
# pylint: disable=too-many-locals,too-many-branches
127128
def aks_agent(
128129
cmd,
129130
resource_group_name,
@@ -177,13 +178,25 @@ def aks_agent(
177178

178179
# Detect and read piped input
179180
piped_data = None
180-
if not sys.stdin.isatty():
181-
piped_data = sys.stdin.read().strip()
182-
if interactive:
183-
console.print(
184-
"[bold yellow]Interactive mode disabled when reading piped input[/bold yellow]"
185-
)
186-
interactive = False
181+
# In non-interactive mode with a prompt, we shouldn't try to read stdin
182+
# as it may hang in CI/CD environments. Only read stdin if:
183+
# 1. Not a TTY (indicating piped input)
184+
# 2. Interactive mode is enabled (allows stdin reading)
185+
should_check_stdin = not sys.stdin.isatty() and interactive
186+
187+
if should_check_stdin:
188+
try:
189+
# Use select with timeout to avoid hanging
190+
# Check if data is available with 100ms timeout
191+
if select.select([sys.stdin], [], [], 0.1)[0]:
192+
piped_data = sys.stdin.read().strip()
193+
console.print(
194+
"[bold yellow]Interactive mode disabled when reading piped input[/bold yellow]"
195+
)
196+
interactive = False
197+
except Exception: # pylint: disable=broad-exception-caught
198+
# Continue without piped data if stdin reading fails
199+
pass
187200

188201
# Determine MCP mode and smart refresh logic
189202
use_aks_mcp = bool(use_aks_mcp)

src/aks-agent/azext_aks_agent/tests/evals/test_ask_agent.py

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
import os
99
import shlex
1010
import subprocess
11+
import textwrap
12+
import sys
13+
import threading
14+
from datetime import datetime, timezone
1115
from pathlib import Path
16+
from time import perf_counter
1217
from typing import Iterable
1318

1419

@@ -40,6 +45,26 @@
4045
ITERATIONS = int(os.environ.get("ITERATIONS", "1"))
4146
BRAINTRUST_UPLOADER = BraintrustUploader(os.environ)
4247

48+
49+
def _log(message: str) -> None:
50+
"""Emit a timestamped log line that pytest `-s` will surface immediately."""
51+
timestamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
52+
print(f"[{timestamp}] {message}", flush=True)
53+
54+
55+
def _summarise_command(parts: Iterable[str]) -> str:
56+
"""Return a shell-style command string for debugging output."""
57+
sequence = parts if isinstance(parts, list) else list(parts)
58+
if hasattr(shlex, "join"):
59+
return shlex.join(sequence)
60+
# ``shlex.join`` was added in Python 3.8; keep a safe fallback just in case.
61+
return " ".join(shlex.quote(part) for part in sequence)
62+
63+
64+
def _preview_output(output: str, *, limit: int = 400) -> str:
65+
"""Provide a trimmed preview of command output for quick debugging."""
66+
return textwrap.shorten(output.strip(), width=limit, placeholder=" …")
67+
4368
pytestmark = [
4469
pytest.mark.skipif(
4570
not RUN_LIVE,
@@ -90,14 +115,59 @@ def _build_command(prompt: str, model: str, resource_group: str, cluster_name: s
90115

91116

92117
def _run_cli(command: Iterable[str], env: dict[str, str]) -> str:
118+
command_list = list(command)
119+
command_display = _summarise_command(command_list)
120+
_log(f"Invoking AKS Agent CLI: {command_display}")
121+
start = perf_counter()
122+
123+
timeout_seconds = 600 # 10 minutes timeout
124+
93125
try:
94-
result = subprocess.run( # noqa: S603
95-
list(command),
96-
check=True,
97-
capture_output=True,
126+
# Use Popen for real-time output visibility
127+
process = subprocess.Popen( # noqa: S603
128+
command_list,
129+
stdout=subprocess.PIPE,
130+
stderr=subprocess.PIPE,
98131
text=True,
99132
env=env,
100133
)
134+
135+
# Thread to print stderr in real-time
136+
stderr_lines = []
137+
def print_stderr():
138+
for line in iter(process.stderr.readline, ''):
139+
if line:
140+
print(f"[STDERR] {line.rstrip()}", file=sys.stderr, flush=True)
141+
stderr_lines.append(line)
142+
143+
stderr_thread = threading.Thread(target=print_stderr, daemon=True)
144+
stderr_thread.start()
145+
146+
# Wait with timeout
147+
try:
148+
stdout, _ = process.communicate(timeout=timeout_seconds)
149+
stderr_thread.join(timeout=1)
150+
stderr = ''.join(stderr_lines)
151+
except subprocess.TimeoutExpired:
152+
process.kill()
153+
stdout, stderr_remainder = process.communicate()
154+
stderr = ''.join(stderr_lines) + (stderr_remainder or '')
155+
_log(f"[ERROR] CLI command timed out after {timeout_seconds}s")
156+
pytest.fail(
157+
f"AKS Agent CLI call timed out after {timeout_seconds}s\n"
158+
f"Command: {command_display}\n"
159+
f"Stdout: {stdout}\n"
160+
f"Stderr: {stderr}"
161+
)
162+
163+
if process.returncode != 0:
164+
raise subprocess.CalledProcessError(
165+
process.returncode, command_list, stdout, stderr
166+
)
167+
168+
result = subprocess.CompletedProcess(
169+
command_list, process.returncode, stdout, stderr
170+
)
101171
except subprocess.CalledProcessError as exc: # pragma: no cover - live failure path
102172
output = exc.stdout or ""
103173
stderr = exc.stderr or ""
@@ -109,13 +179,28 @@ def _run_cli(command: Iterable[str], env: dict[str, str]) -> str:
109179
f"Stdout: {output}\n"
110180
f"Stderr: {stderr}"
111181
)
182+
duration = perf_counter() - start
183+
stdout_preview = _preview_output(result.stdout)
184+
stderr_preview = _preview_output(result.stderr) if result.stderr else None
185+
_log(
186+
f"AKS Agent CLI completed in {duration:.1f}s with stdout preview: {stdout_preview}"
187+
)
188+
if stderr_preview:
189+
_log(
190+
f"AKS Agent CLI stderr preview: {stderr_preview}"
191+
)
112192
return result.stdout
113193

114194

115195
def _run_commands(
116196
commands: list[str], env: dict[str, str], label: str, scenario: Scenario
117197
) -> None:
198+
if not commands:
199+
_log(f"[{label}] {scenario.name}: no commands to run")
200+
return
118201
for cmd in commands:
202+
_log(f"[{label}] {scenario.name}: running shell command: {cmd}")
203+
start = perf_counter()
119204
try:
120205
completed = subprocess.run( # noqa: S603
121206
cmd,
@@ -137,9 +222,25 @@ def _run_commands(
137222
f"Stderr: {stderr}"
138223
)
139224
else:
225+
duration = perf_counter() - start
140226
# Provide quick visibility into command results when debugging failures.
141227
if completed.stdout:
142-
print(f"[{label}] {scenario.name}: {completed.stdout.strip()}")
228+
stdout_preview = _preview_output(completed.stdout)
229+
_log(
230+
f"[{label}] {scenario.name}: succeeded in {duration:.1f}s; stdout preview: {stdout_preview}"
231+
)
232+
else:
233+
_log(
234+
f"[{label}] {scenario.name}: succeeded in {duration:.1f}s; no stdout produced"
235+
)
236+
if completed.stderr:
237+
stderr_preview = _preview_output(completed.stderr)
238+
_log(
239+
f"[{label}] {scenario.name}: stderr preview: {stderr_preview}"
240+
)
241+
_log(
242+
f"[{label}] {scenario.name}: completed {len(commands)} command(s)"
243+
)
143244

144245

145246
def _scenario_params() -> list:
@@ -165,6 +266,7 @@ def test_ask_agent_live(
165266
request: pytest.FixtureRequest,
166267
) -> None:
167268
iteration_label = f"[iteration {iteration + 1}/{ITERATIONS}]"
269+
_log(f"{iteration_label} starting scenario {scenario.name}")
168270
if RUN_LIVE:
169271
env = _load_env()
170272

@@ -178,7 +280,7 @@ def test_ask_agent_live(
178280
env.update(scenario.env_overrides)
179281

180282
if iteration == 0 and scenario.before_commands and not aks_skip_setup:
181-
print(f"{iteration_label} running setup commands for {scenario.name}")
283+
_log(f"{iteration_label} running setup commands for {scenario.name}")
182284
_run_commands(scenario.before_commands, env, "setup", scenario)
183285

184286
command = _build_command(
@@ -188,7 +290,7 @@ def test_ask_agent_live(
188290
cluster_name=cluster_name,
189291
)
190292

191-
print(f"{iteration_label} invoking AKS Agent CLI for {scenario.name}")
293+
_log(f"{iteration_label} invoking AKS Agent CLI for {scenario.name}")
192294
try:
193295
raw_output = _run_cli(command, env)
194296
answer = ""
@@ -216,11 +318,11 @@ def test_ask_agent_live(
216318
classifier_rationale = classifier_result.metadata.get(
217319
"rationale", ""
218320
)
219-
print(
321+
_log(
220322
f"{iteration_label} classifier score for {scenario.name}: {classifier_score}"
221323
)
222324
if classifier_score is None:
223-
print(
325+
_log(
224326
f"{iteration_label} classifier returned no score for {scenario.name}; falling back to substring checks"
225327
)
226328
else:
@@ -230,7 +332,7 @@ def test_ask_agent_live(
230332
if not error_message:
231333
error_message = "Classifier judged answer incorrect"
232334
else:
233-
print(
335+
_log(
234336
f"{iteration_label} classifier unavailable for {scenario.name}; falling back to substring checks"
235337
)
236338

@@ -280,21 +382,21 @@ def test_ask_agent_live(
280382

281383
if GENERATE_MOCKS:
282384
mock_path = save_mock_answer(scenario.mock_path, answer)
283-
print(f"{iteration_label} [mock] wrote response to {mock_path}")
385+
_log(f"{iteration_label} [mock] wrote response to {mock_path}")
284386
finally:
285387
if (
286388
iteration == ITERATIONS - 1
287389
and scenario.after_commands
288390
and not aks_skip_cleanup
289391
):
290-
print(f"{iteration_label} running cleanup commands for {scenario.name}")
392+
_log(f"{iteration_label} running cleanup commands for {scenario.name}")
291393
_run_commands(scenario.after_commands, env, "cleanup", scenario)
292394
else:
293395
if GENERATE_MOCKS:
294396
pytest.fail("GENERATE_MOCKS requires RUN_LIVE=true")
295397
try:
296398
answer = load_mock_answer(scenario.mock_path)
297-
print(f"{iteration_label} replayed mock response for {scenario.name}")
399+
_log(f"{iteration_label} replayed mock response for {scenario.name}")
298400
except FileNotFoundError:
299401
pytest.skip(f"Mock response missing for scenario {scenario.name}; rerun with RUN_LIVE=true GENERATE_MOCKS=true")
300402

@@ -328,5 +430,6 @@ def test_ask_agent_live(
328430
_set_user_property(request, 'braintrust_root_span_id', str(root_span_id))
329431
if url:
330432
_set_user_property(request, 'braintrust_experiment_url', str(url))
433+
_log(f"{iteration_label} completed scenario {scenario.name} (passed={passed})")
331434
if not passed:
332435
pytest.fail(f"Scenario {scenario.name}: {error}\nAI answer:\n{answer}")

src/aks-agent/azext_aks_agent/tests/latest/test_aks_agent.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,8 @@
66
import os
77
import sys
88
import unittest
9-
from types import SimpleNamespace
10-
from unittest.mock import MagicMock, Mock, call, patch
9+
from unittest.mock import Mock, patch
1110

12-
from azext_aks_agent._consts import (CONST_AGENT_CONFIG_PATH_DIR_ENV_KEY,
13-
CONST_AGENT_NAME,
14-
CONST_AGENT_NAME_ENV_KEY)
1511
from azext_aks_agent.agent.agent import aks_agent, init_log
1612
from azure.cli.core.util import CLIError
1713

src/aks-agent/setup.cfg

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
[bdist_wheel]
22
universal=1
3+
4+
[tool:pytest]
5+
markers =
6+
easy: Regression AKS Agent evals that should always pass
7+
medium: Stretch AKS Agent evals that may fail occasionally
8+
hard: Challenging AKS Agent evals reserved for complex scenarios
9+
kubernetes: AKS Agent evals that exercise Kubernetes-focused flows
10+
aks_eval: AKS Agent evaluation tests

src/aks-preview/HISTORY.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ To release a new version, please select a new version number (usually plus 1 to
1111

1212
Pending
1313
+++++++
14+
* Support `entraid` for parameter `--ssh-access` to support EntraID feature.
1415

1516
19.0.0b6
1617
+++++++
17-
* Support `entraid` for parameter `--ssh-access` to support EntraID feature.
18+
* Update the minimum required cli core version to `2.73.0` (actually since `18.0.0b35`).
1819

1920
19.0.0b5
2021
+++++++

src/aks-preview/README.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ Dependency between aks-preview and azure-cli/acs (azure-cli-core)
4949
- >= `\2.49.0 <https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.49.0>`_, 2023/05/23
5050
* - 2.0.0b7 ~ 7.0.0b1
5151
- >= `\2.56.0 <https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.56.0>`_, 2024/01/09
52-
* - 7.0.0b2 ~ latest
52+
* - 7.0.0b2 ~ 18.0.0b34
5353
- >= `\2.61.0 <https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.61.0>`_, 2024/05/21
54+
* - 18.0.0b35 ~ latest
55+
- >= `\2.73.0 <https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.73.0>`_, 2025/05/19
5456

5557
Released version and adopted API version
5658
========================================
@@ -209,6 +211,9 @@ Released version and adopted API version
209211
* - 18.0.0b22 ~ 18.0.0b34
210212
- 2025-06-02-preview
211213
-
212-
* - 18.0.0b35 ~ latest
214+
* - 18.0.0b35 ~ 18.0.0b43
213215
- 2025-07-02-preview
216+
-
217+
* - 18.0.0b44 ~ latest
218+
- 2025-08-02-preview
214219
-
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"azext.minCliCoreVersion": "2.61.0",
2+
"azext.minCliCoreVersion": "2.73.0",
33
"azext.isPreview": true,
44
"name": "aks-preview"
55
}

src/confcom/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
logger.warn("Wheel is not available, disabling bdist_wheel hook")
2121

22-
VERSION = "1.3.0"
22+
VERSION = "1.3.1"
2323

2424
# The full list of classifiers is available at
2525
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
@@ -40,7 +40,7 @@
4040
DEPENDENCIES = [
4141
"docker>=6.1.0",
4242
"tqdm==4.65.0",
43-
"deepdiff==6.3.0",
43+
"deepdiff~=8.6.1",
4444
"PyYAML>=6.0.1"
4545
]
4646

0 commit comments

Comments
 (0)