-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add CLI shell and exec commands for pod terminal access #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
44ee17b
c62f890
095bd1e
d03f493
3dadb75
8a93733
0a4c1bb
7259bd4
ab51beb
8770660
b79a30a
0b03a53
54445b8
0a0636c
31d41ae
ec8286b
5d94f14
559460f
ff0e893
c6d42ed
ba0e9d5
f29df94
db40469
f0b37b8
4265839
27977af
1857f89
76fd598
a531f00
e9e3829
423cdc7
9ef67f5
59d9d08
c49e5c5
dd00cbb
4e01d82
3ff11ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """CLI commands for interactive shell and command execution in deployment pods.""" | ||
|
|
||
| import asyncio | ||
| import sys | ||
|
|
||
| import click | ||
|
|
||
| from centml.cli.cluster import handle_exception | ||
| from centml.sdk import auth | ||
| from centml.sdk.api import get_centml_client | ||
| from centml.sdk.config import settings | ||
| from centml.sdk.shell import ( | ||
| PodNotFoundError, | ||
| ShellError, | ||
| build_ws_url, | ||
| exec_session, | ||
| get_running_pods, | ||
| interactive_session, | ||
| ) | ||
|
|
||
|
|
||
| def _resolve_pod(running_pods: list[str], pod_name: str) -> str: | ||
| """Validate that *pod_name* exists in *running_pods*.""" | ||
| if pod_name not in running_pods: | ||
| pods_list = ", ".join(running_pods) | ||
| raise PodNotFoundError(f"Pod '{pod_name}' not found. Available running pods: {pods_list}") | ||
| return pod_name | ||
|
|
||
|
|
||
| def _select_pod(running_pods, deployment_id): | ||
| click.echo(f"Multiple running pods found for deployment {deployment_id}:") | ||
| for i, name in enumerate(running_pods, 1): | ||
| click.echo(f" [{i}] {name}") | ||
|
|
||
| choice = click.prompt( | ||
| "Select a pod", type=click.IntRange(1, len(running_pods)), prompt_suffix=f" [1-{len(running_pods)}]: " | ||
| ) | ||
| return running_pods[choice - 1] | ||
|
|
||
|
|
||
| def _connect_args(deployment_id, pod, shell_type, first_pod=False): | ||
| """Resolve pod, build WebSocket URL, and obtain auth token.""" | ||
| with get_centml_client() as cclient: | ||
| running_pods = get_running_pods(cclient, deployment_id) | ||
| if not running_pods: | ||
| raise click.ClickException(f"No running pods found for deployment {deployment_id}") | ||
|
|
||
| if pod is not None: | ||
| try: | ||
| pod_name = _resolve_pod(running_pods, pod) | ||
| except ShellError as exc: | ||
| raise click.ClickException(str(exc)) from exc | ||
| elif len(running_pods) == 1 or first_pod: | ||
| pod_name = running_pods[0] | ||
| else: | ||
| pod_name = _select_pod(running_pods, deployment_id) | ||
|
Comment on lines
+41
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, let's understand the structure of the file and locate the key functions
cd /repo
find . -name "shell.py" -path "*/cli/*" 2>/dev/null | head -5Repository: CentML/centml-python-client Length of output: 165 🏁 Script executed: #!/bin/bash
# Read the entire shell.py file to understand the context
wc -l centml/cli/shell.pyRepository: CentML/centml-python-client Length of output: 93 🏁 Script executed: #!/bin/bash
# Get the content of lines 1-100 to see the function definitions and calls
cat -n centml/cli/shell.py | head -100Repository: CentML/centml-python-client Length of output: 4356 Avoid prompting from The 🤖 Prompt for AI Agents |
||
|
|
||
| ws_url = build_ws_url(settings.CENTML_PLATFORM_API_URL, deployment_id, pod_name, shell_type) | ||
| token = auth.get_centml_token() | ||
| return ws_url, token | ||
|
|
||
|
|
||
| @click.command(help="Open an interactive shell to a deployment pod") | ||
| @click.argument("deployment_id", type=int) | ||
| @click.option("--pod", default=None, help="Specify a pod name") | ||
| @click.option("--shell", "shell_type", default=None, type=click.Choice(["bash", "sh", "zsh"]), help="Shell type") | ||
| @click.option( | ||
| "--first-pod", is_flag=True, default=False, help="Auto-select the first running pod (skip interactive selection)" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need this right? if --pod is not provided, then we default to first pod |
||
| ) | ||
| @handle_exception | ||
| def shell(deployment_id, pod, shell_type, first_pod): | ||
| if not sys.stdin.isatty(): | ||
| raise click.ClickException("Interactive shell requires a terminal (TTY)") | ||
|
|
||
| ws_url, token = _connect_args(deployment_id, pod, shell_type, first_pod) | ||
| exit_code = asyncio.run(interactive_session(ws_url, token)) | ||
| sys.exit(exit_code) | ||
|
|
||
|
|
||
| @click.command(help="Execute a command in a deployment pod", context_settings={"ignore_unknown_options": True}) | ||
| @click.argument("deployment_id", type=int) | ||
| @click.argument("command", nargs=-1, required=True, type=click.UNPROCESSED) | ||
| @click.option("--pod", default=None, help="Specific pod name") | ||
| @click.option("--shell", "shell_type", default=None, type=click.Choice(["bash", "sh", "zsh"]), help="Shell type") | ||
| @click.option( | ||
| "--first-pod", is_flag=True, default=False, help="Auto-select the first running pod (skip interactive selection)" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here |
||
| ) | ||
| @handle_exception | ||
| def exec_cmd(deployment_id, command, pod, shell_type, first_pod): | ||
| ws_url, token = _connect_args(deployment_id, pod, shell_type, first_pod) | ||
| exit_code = asyncio.run(exec_session(ws_url, token, " ".join(command))) | ||
anandj91 marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+89
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's look at the file and the specific lines mentioned
cat -n centml/cli/shell.py | sed -n '80,100p'Repository: CentML/centml-python-client Length of output: 965 🏁 Script executed: # Also check the imports at the top of the file
head -20 centml/cli/shell.pyRepository: CentML/centml-python-client Length of output: 510 🏁 Script executed: # Let's understand the broader context of the exec_cmd function
cat -n centml/cli/shell.py | sed -n '85,110p'Repository: CentML/centml-python-client Length of output: 522 🏁 Script executed: # Check if shlex is already imported anywhere in the file
grep -n "import shlex" centml/cli/shell.pyRepository: CentML/centml-python-client Length of output: 53 🏁 Script executed: # Search for how command is passed to exec_cmd - look for Click decorators
cat -n centml/cli/shell.py | sed -n '1,100p'Repository: CentML/centml-python-client Length of output: 4356 🏁 Script executed: # Check Python version requirements
fd -t f -e "toml" -e "cfg" -e "txt" | xargs grep -l "python" 2>/dev/null | head -5Repository: CentML/centml-python-client Length of output: 87 🏁 Script executed: # Look for pyproject.toml or setup.py
ls -la centml/ | head -20
git ls-files | grep -E "(pyproject|setup)" | head -10Repository: CentML/centml-python-client Length of output: 409 🏁 Script executed: # Check the project config
cat pyproject.toml 2>/dev/null || cat setup.py 2>/dev/nullRepository: CentML/centml-python-client Length of output: 747 Use The Suggested fix+import shlex
...
- exit_code = asyncio.run(exec_session(ws_url, token, " ".join(command)))
+ exit_code = asyncio.run(exec_session(ws_url, token, shlex.join(command)))🤖 Prompt for AI Agents |
||
| sys.exit(exit_code) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from centml.sdk.shell.exceptions import NoPodAvailableError, PodNotFoundError, ShellError | ||
| from centml.sdk.shell.session import build_ws_url, exec_session, get_running_pods, interactive_session | ||
|
|
||
| __all__ = [ | ||
| "ShellError", | ||
| "NoPodAvailableError", | ||
| "PodNotFoundError", | ||
| "build_ws_url", | ||
| "get_running_pods", | ||
| "interactive_session", | ||
| "exec_session", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| class ShellError(Exception): | ||
| """Base exception for shell operations.""" | ||
|
|
||
|
|
||
| class NoPodAvailableError(ShellError): | ||
| """No running pods found for the deployment.""" | ||
|
|
||
|
|
||
| class PodNotFoundError(ShellError): | ||
| """Specified pod not found among running pods.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this check is also handled at the API. we should be able to remove it from the client