|
4 | 4 | import json |
5 | 5 | import os |
6 | 6 | import platform |
| 7 | +import sys |
7 | 8 | import time |
8 | 9 | from argparse import REMAINDER |
9 | 10 | from typing import TYPE_CHECKING |
|
21 | 22 | WorkloadStatus, |
22 | 23 | create_workload, |
23 | 24 | delete_workload, |
| 25 | + exec_workload, |
24 | 26 | get_workload, |
25 | 27 | list_workloads, |
26 | 28 | logs_workload, |
@@ -567,22 +569,132 @@ def __init__(self, args: Namespace): |
567 | 569 | def run(self): |
568 | 570 | try: |
569 | 571 | print("\033[2J\033[H", end="") |
570 | | - logs_stream = logs_workload(self.name, tail=self.tail, follow=self.follow) |
| 572 | + logs_stream = logs_workload( |
| 573 | + self.name, |
| 574 | + tail=self.tail, |
| 575 | + follow=self.follow, |
| 576 | + ) |
571 | 577 | with contextlib.closing(logs_stream) as logs: |
572 | 578 | for line in logs: |
573 | 579 | print(line.decode("utf-8").rstrip()) |
574 | 580 | except KeyboardInterrupt: |
575 | 581 | print("\033[2J\033[H", end="") |
576 | 582 |
|
577 | 583 |
|
| 584 | +class ExecWorkloadSubCommand(SubCommand): |
| 585 | + """ |
| 586 | + Command to execute a command in a workload deployment. |
| 587 | + """ |
| 588 | + |
| 589 | + name: str |
| 590 | + command: list[str] |
| 591 | + interactive: bool = False |
| 592 | + |
| 593 | + @staticmethod |
| 594 | + def register(parser: _SubParsersAction): |
| 595 | + exec_parser = parser.add_parser( |
| 596 | + "exec", |
| 597 | + help="execute a command in a workload deployment", |
| 598 | + ) |
| 599 | + |
| 600 | + exec_parser.add_argument( |
| 601 | + "name", |
| 602 | + type=str, |
| 603 | + help="name of the workload", |
| 604 | + ) |
| 605 | + |
| 606 | + exec_parser.add_argument( |
| 607 | + "command", |
| 608 | + nargs=REMAINDER, |
| 609 | + help="command to execute in the workload", |
| 610 | + ) |
| 611 | + |
| 612 | + exec_parser.add_argument( |
| 613 | + "--interactive", |
| 614 | + "-i", |
| 615 | + action="store_true", |
| 616 | + help="interactive mode", |
| 617 | + ) |
| 618 | + |
| 619 | + exec_parser.set_defaults(func=ExecWorkloadSubCommand) |
| 620 | + |
| 621 | + def __init__(self, args: Namespace): |
| 622 | + self.name = args.name |
| 623 | + self.command = args.command |
| 624 | + self.interactive = args.interactive |
| 625 | + |
| 626 | + if not self.name: |
| 627 | + msg = "The name argument is required." |
| 628 | + raise ValueError(msg) |
| 629 | + |
| 630 | + def run(self): |
| 631 | + try: |
| 632 | + if self.interactive: |
| 633 | + from dockerpty import io, pty # noqa: PLC0415 |
| 634 | + except ImportError: |
| 635 | + print( |
| 636 | + "dockerpty is required for interactive mode. " |
| 637 | + "Please install it via 'pip install dockerpty'.", |
| 638 | + ) |
| 639 | + sys.exit(1) |
| 640 | + |
| 641 | + try: |
| 642 | + print("\033[2J\033[H", end="") |
| 643 | + exec_result = exec_workload( |
| 644 | + self.name, |
| 645 | + detach=not self.interactive, |
| 646 | + command=self.command, |
| 647 | + ) |
| 648 | + |
| 649 | + # Non-interactive mode: print output and exit with the command's exit code |
| 650 | + |
| 651 | + if not self.interactive: |
| 652 | + if isinstance(exec_result.output, bytes): |
| 653 | + print(exec_result.output.decode("utf-8").rstrip()) |
| 654 | + else: |
| 655 | + print(exec_result.output) |
| 656 | + sys.exit(exec_result.exit_code) |
| 657 | + |
| 658 | + # Interactive mode: use dockerpty to attach to the exec session |
| 659 | + |
| 660 | + class ExecOperation(pty.Operation): |
| 661 | + def __init__(self, socket): |
| 662 | + self.stdin = sys.stdin |
| 663 | + self.stdout = sys.stdout |
| 664 | + self.socket = io.Stream(socket) |
| 665 | + |
| 666 | + def israw(self, **_): |
| 667 | + return self.stdout.isatty() |
| 668 | + |
| 669 | + def start(self, **_): |
| 670 | + stream = self.sockets() |
| 671 | + return [ |
| 672 | + io.Pump(io.Stream(self.stdin), stream, wait_for_output=False), |
| 673 | + io.Pump(stream, io.Stream(self.stdout), propagate_close=False), |
| 674 | + ] |
| 675 | + |
| 676 | + def resize(self, height, width, **_): |
| 677 | + pass |
| 678 | + |
| 679 | + def sockets(self): |
| 680 | + return self.socket |
| 681 | + |
| 682 | + exec_op = ExecOperation(exec_result.output) |
| 683 | + pty.PseudoTerminal(None, exec_op).start() |
| 684 | + except KeyboardInterrupt: |
| 685 | + print("\033[2J\033[H", end="") |
| 686 | + |
| 687 | + |
578 | 688 | def format_workloads_json(sts: list[WorkloadStatus]) -> str: |
579 | 689 | return json.dumps([st.__dict__ for st in sts], indent=2) |
580 | 690 |
|
581 | 691 |
|
582 | | -def format_workloads_table(sts: list[WorkloadStatus], width: int = 100) -> str: |
| 692 | +def format_workloads_table(sts: list[WorkloadStatus]) -> str: |
583 | 693 | if not sts: |
584 | 694 | return "No workloads found." |
585 | 695 |
|
| 696 | + width = 100 |
| 697 | + |
586 | 698 | headers = ["Name", "State", "Created At"] |
587 | 699 | col_widths = [ |
588 | 700 | len(str(getattr(st, attr.lower().replace(" ", "_")))) |
|
0 commit comments