Skip to content

Commit 3413262

Browse files
authored
{AKS} az aks bastion: Fix default subshell on windows platform (#9280)
1 parent 2bc0770 commit 3413262

File tree

2 files changed

+107
-1
lines changed

2 files changed

+107
-1
lines changed

src/aks-preview/HISTORY.rst

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

1212
Pending
1313
+++++++
14+
* Fix `az aks bastion` subshell defaulting to cmd on Windows when invoked from PowerShell by implementing grandparent process detection to identify the actual user shell.
1415

1516
19.0.0b8
1617
+++++++

src/aks-preview/azext_aks_preview/bastion/bastion.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import asyncio
77
import os
8+
import shutil
89
import socket
910
import subprocess
1011
import sys
@@ -239,19 +240,121 @@ def _aks_bastion_get_current_shell_cmd():
239240

240241
ppid = os.getppid()
241242
parent = psutil.Process(ppid)
242-
return parent.name()
243+
parent_name = parent.name()
244+
logger.debug("Immediate parent process: %s (PID: %s)", parent_name, ppid)
245+
246+
# On Windows, Azure CLI is often invoked as az.cmd, which means the immediate parent
247+
# is cmd.exe but the actual user shell (PowerShell) is the grandparent process
248+
if not sys.platform.startswith("win"):
249+
logger.debug("Using parent process name as shell: %s", parent_name)
250+
return parent_name
251+
252+
return _get_windows_shell_cmd(parent, parent_name)
253+
254+
255+
def _get_windows_shell_cmd(parent, parent_name):
256+
"""Get the shell command on Windows, handling az.cmd wrapper scenarios."""
257+
try:
258+
parent_exe = parent.exe()
259+
logger.debug("Parent executable path: %s", parent_exe)
260+
261+
# If the immediate parent is cmd.exe, check if it's wrapping az.cmd for PowerShell
262+
if "cmd" in parent_name.lower():
263+
return _handle_cmd_parent(parent)
264+
265+
# For direct PowerShell processes (not wrapped by cmd)
266+
if "pwsh" in parent_name.lower() or "powershell" in parent_name.lower():
267+
return _handle_powershell_parent(parent_exe, parent_name)
268+
269+
logger.debug("Other Windows shell detected: %s", parent_name)
270+
return parent_exe if parent_exe else parent_name
271+
272+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
273+
logger.debug("Cannot access parent process details: %s", e)
274+
return parent_name
275+
276+
277+
def _handle_cmd_parent(parent):
278+
"""Handle case where immediate parent is cmd.exe - check for PowerShell grandparent."""
279+
try:
280+
# Get the grandparent process (parent of cmd.exe)
281+
grandparent = parent.parent()
282+
if not grandparent:
283+
return "cmd"
284+
285+
grandparent_name = grandparent.name()
286+
logger.debug("Detected grandparent process: %s (PID: %s)", grandparent_name, grandparent.pid)
287+
288+
# If grandparent is PowerShell, that's the actual user shell
289+
if "pwsh" in grandparent_name.lower() or "powershell" in grandparent_name.lower():
290+
return _get_powershell_executable(grandparent)
291+
292+
logger.debug("Grandparent is not PowerShell - using cmd as target shell")
293+
return "cmd"
294+
295+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
296+
# If we can't access grandparent, assume cmd is the actual shell
297+
logger.debug("Cannot access grandparent process: %s - using cmd as target shell", e)
298+
return "cmd"
299+
300+
301+
def _handle_powershell_parent(parent_exe, parent_name):
302+
"""Handle direct PowerShell parent process."""
303+
logger.debug("Direct PowerShell parent detected")
304+
return _get_powershell_executable_from_path() or parent_exe or parent_name
305+
306+
307+
def _get_powershell_executable(grandparent):
308+
"""Get PowerShell executable, preferring pwsh over powershell."""
309+
logger.debug("Grandparent is PowerShell - using PowerShell as target shell")
310+
powershell_cmd = _get_powershell_executable_from_path()
311+
if powershell_cmd:
312+
return powershell_cmd
313+
314+
# If we can't find pwsh/powershell in PATH, use the detected grandparent
315+
logger.debug("PowerShell not found in PATH, using detected grandparent executable")
316+
return grandparent.exe() if grandparent.exe() else grandparent.name()
317+
318+
319+
def _get_powershell_executable_from_path():
320+
"""Try to find PowerShell executable in PATH, preferring pwsh over powershell."""
321+
pwsh_path = shutil.which("pwsh")
322+
if pwsh_path:
323+
logger.debug("Found pwsh at: %s", pwsh_path)
324+
return "pwsh"
325+
326+
powershell_path = shutil.which("powershell")
327+
if powershell_path:
328+
logger.debug("Found powershell at: %s", powershell_path)
329+
return "powershell"
330+
331+
return None
243332

244333

245334
def _aks_bastion_prepare_shell_cmd(kubeconfig_path):
246335
"""Prepare the shell command to launch a subshell with KUBECONFIG set."""
247336

248337
shell_cmd = _aks_bastion_get_current_shell_cmd()
249338
updated_shell_cmd = shell_cmd
339+
340+
# Handle different shell types
250341
if shell_cmd.endswith("bash") and os.path.exists(os.path.expanduser("~/.bashrc")):
251342
updated_shell_cmd = (
252343
f"""{shell_cmd} -c '{shell_cmd} --rcfile <(cat ~/.bashrc; """
253344
f"""echo "export KUBECONFIG={kubeconfig_path}")'"""
254345
)
346+
elif shell_cmd in ["pwsh", "powershell"] or "pwsh" in shell_cmd.lower() or "powershell" in shell_cmd.lower():
347+
# PowerShell: Set environment variable and start new session
348+
# Use proper PowerShell syntax for setting environment variables
349+
escaped_path = kubeconfig_path.replace("'", "''") # Escape single quotes for PowerShell
350+
if shell_cmd == "pwsh" or "pwsh" in shell_cmd.lower():
351+
updated_shell_cmd = f'pwsh -NoExit -Command "$env:KUBECONFIG=\'{escaped_path}\'"'
352+
else:
353+
updated_shell_cmd = f'powershell -NoExit -Command "$env:KUBECONFIG=\'{escaped_path}\'"'
354+
elif shell_cmd == "cmd" or "cmd" in shell_cmd.lower():
355+
# CMD: Set environment variable and keep session open
356+
updated_shell_cmd = f'cmd /k "set KUBECONFIG={kubeconfig_path}"'
357+
255358
return shell_cmd, updated_shell_cmd
256359

257360

@@ -260,6 +363,8 @@ def _aks_bastion_restore_shell(shell_cmd):
260363

261364
if shell_cmd.endswith("bash"):
262365
subprocess.run(["stty", "sane"], stdin=sys.stdin)
366+
# PowerShell and CMD on Windows typically don't need special restoration
367+
# as they handle terminal state management internally
263368

264369

265370
async def _aks_bastion_launch_subshell(kubeconfig_path, port):

0 commit comments

Comments
 (0)