From 8dd947bdb4f7029bf043cb0cfebe494c17bf2729 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Wed, 30 Nov 2022 09:27:58 +0800 Subject: [PATCH 1/2] feat(goal): Added algokit goal --console --- src/algokit/cli/goal.py | 34 +++++++++++++------ src/algokit/core/exec.py | 29 ++++++++++++++++ tests/goal/test_goal.py | 33 +++++++++++++++--- .../test_goal.test_goal_console.approved.txt | 5 +++ ...goal.test_goal_console_failed.approved.txt | 6 ++++ 5 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 tests/goal/test_goal.test_goal_console.approved.txt create mode 100644 tests/goal/test_goal.test_goal_console_failed.approved.txt diff --git a/src/algokit/cli/goal.py b/src/algokit/cli/goal.py index 1322489e..82e74e9f 100644 --- a/src/algokit/cli/goal.py +++ b/src/algokit/cli/goal.py @@ -13,8 +13,13 @@ "ignore_unknown_options": True, }, ) +@click.option( + "--console/--no-console", + help="Open a Bash console so you can execute multiple goal commands and/or interact with a filesystem", + default=False, +) @click.argument("goal_args", nargs=-1, type=click.UNPROCESSED) -def goal_command(goal_args: list[str]) -> None: +def goal_command(console: bool, goal_args: list[str]) -> None: # noqa: FBT001 try: exec.run(["docker", "version"], bad_return_code_error_message="Docker engine isn't running; please start it.") except IOError as ex: @@ -24,12 +29,21 @@ def goal_command(goal_args: list[str]) -> None: "Docker not found; please install Docker and add to path.\n" "See https://docs.docker.com/get-docker/ for more information." ) from ex - cmd = str("docker exec algokit_algod goal").split() - cmd.extend(goal_args) - exec.run( - cmd, - stdout_log_level=logging.INFO, - prefix_process=False, - bad_return_code_error_message="Error executing goal;" - + " ensure the Sandbox is started by executing `algokit sandbox start`", - ) + if console: + logger.info("Opening Bash console on the algod node; execute `exit` to return to original console") + result = exec.run_interactive("docker exec -it -w /root algokit_algod bash".split()) + if result.exit_code != 0: + raise click.ClickException( + "Error executing goal;" + " ensure the Sandbox is started by executing `algokit sandbox status`" + ) + + else: + cmd = str("docker exec algokit_algod goal").split() + cmd.extend(goal_args) + exec.run( + cmd, + stdout_log_level=logging.INFO, + prefix_process=False, + bad_return_code_error_message="Error executing goal;" + + " ensure the Sandbox is started by executing `algokit sandbox status`", + ) diff --git a/src/algokit/core/exec.py b/src/algokit/core/exec.py index 20cd3b09..a2a0b397 100644 --- a/src/algokit/core/exec.py +++ b/src/algokit/core/exec.py @@ -3,6 +3,7 @@ import subprocess from pathlib import Path from subprocess import Popen +from subprocess import run as subprocess_run import click from algokit.core.log_handlers import EXTRA_EXCLUDE_FROM_CONSOLE @@ -64,3 +65,31 @@ def run( raise click.ClickException(bad_return_code_error_message) output = "".join(lines) return RunResult(command=command_str, exit_code=exit_code, output=output) + + +def run_interactive( + command: list[str], + *, + cwd: Path | None = None, + env: dict[str, str] | None = None, + bad_return_code_error_message: str | None = None, +) -> RunResult: + """Wraps subprocess.run() as an user interactive session and + also adds logging of the command being executed, but not the output + + Note that not all options or usage scenarios here are covered, just some common use cases + """ + command_str = " ".join(command) + logger.debug(f"Running '{command_str}' in '{cwd or Path.cwd()}'") + + result = subprocess_run(command, cwd=cwd, env=env) + + if result.returncode == 0: + logger.debug(f"'{command_str}' completed successfully", extra=EXTRA_EXCLUDE_FROM_CONSOLE) + else: + logger.debug( + f"'{command_str}' failed, exited with code = {result.returncode}", extra=EXTRA_EXCLUDE_FROM_CONSOLE + ) + if bad_return_code_error_message: + raise click.ClickException(bad_return_code_error_message) + return RunResult(command=command_str, exit_code=result.returncode, output="") diff --git a/tests/goal/test_goal.py b/tests/goal/test_goal.py index e917a01a..39902c8b 100644 --- a/tests/goal/test_goal.py +++ b/tests/goal/test_goal.py @@ -1,4 +1,7 @@ +from subprocess import CompletedProcess + from approvaltests import verify # type: ignore +from pytest_mock import MockerFixture from utils.app_dir_mock import AppDirs from utils.click_invoker import invoke from utils.exec_mock import ExecMock @@ -11,21 +14,43 @@ def test_goal_no_args(app_dir_mock: AppDirs, exec_mock: ExecMock): verify(result.output) -def test_goal_simple_args(app_dir_mock: AppDirs, exec_mock: ExecMock): +def test_goal_console(exec_mock: ExecMock, mocker: MockerFixture): + mocker.patch("algokit.core.exec.subprocess_run").return_value = CompletedProcess( + ["docker", "exec"], 0, "STDOUT+STDERR" + ) + + result = invoke("goal --console") + + assert result.exit_code == 0 + verify(result.output) + + +def test_goal_console_failed(exec_mock: ExecMock, mocker: MockerFixture): + mocker.patch("algokit.core.exec.subprocess_run").return_value = CompletedProcess( + ["docker", "exec"], 1, "STDOUT+STDERR" + ) + + result = invoke("goal --console") + + assert result.exit_code == 1 + verify(result.output) + + +def test_goal_simple_args(exec_mock: ExecMock): result = invoke("goal account list") assert result.exit_code == 0 verify(result.output) -def test_goal_complex_args(app_dir_mock: AppDirs, exec_mock: ExecMock): +def test_goal_complex_args(exec_mock: ExecMock): result = invoke("goal account export -a RKTAZY2ZLKUJBHDVVA3KKHEDK7PRVGIGOZAUUIZBNK2OEP6KQGEXKKUYUY") assert result.exit_code == 0 verify(result.output) -def test_goal_start_without_docker(app_dir_mock: AppDirs, exec_mock: ExecMock): +def test_goal_start_without_docker(exec_mock: ExecMock): exec_mock.should_fail_on("docker version") result = invoke("goal") @@ -34,7 +59,7 @@ def test_goal_start_without_docker(app_dir_mock: AppDirs, exec_mock: ExecMock): verify(result.output) -def test_goal_start_without_docker_engine_running(app_dir_mock: AppDirs, exec_mock: ExecMock): +def test_goal_start_without_docker_engine_running(exec_mock: ExecMock): exec_mock.should_bad_exit_on("docker version") result = invoke("goal") diff --git a/tests/goal/test_goal.test_goal_console.approved.txt b/tests/goal/test_goal.test_goal_console.approved.txt new file mode 100644 index 00000000..416140d0 --- /dev/null +++ b/tests/goal/test_goal.test_goal_console.approved.txt @@ -0,0 +1,5 @@ +DEBUG: Running 'docker version' in '{current_working_directory}' +DEBUG: docker: STDOUT +DEBUG: docker: STDERR +Opening Bash console on the algod node; execute `exit` to return to original console +DEBUG: Running 'docker exec -it -w /root algokit_algod bash' in '{current_working_directory}' diff --git a/tests/goal/test_goal.test_goal_console_failed.approved.txt b/tests/goal/test_goal.test_goal_console_failed.approved.txt new file mode 100644 index 00000000..71127933 --- /dev/null +++ b/tests/goal/test_goal.test_goal_console_failed.approved.txt @@ -0,0 +1,6 @@ +DEBUG: Running 'docker version' in '{current_working_directory}' +DEBUG: docker: STDOUT +DEBUG: docker: STDERR +Opening Bash console on the algod node; execute `exit` to return to original console +DEBUG: Running 'docker exec -it -w /root algokit_algod bash' in '{current_working_directory}' +Error: Error executing goal; ensure the Sandbox is started by executing `algokit sandbox status` From 95565dff45a6751f960ebfba64fb9ed001a67260 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Wed, 30 Nov 2022 19:16:21 +0800 Subject: [PATCH 2/2] feat(sandbox): Added `algokit sandbox console` --- src/algokit/cli/sandbox.py | 11 +++++++++++ tests/sandbox/test_sandbox_console.py | 17 +++++++++++++++++ ...ndbox_console.test_goal_console.approved.txt | 10 ++++++++++ 3 files changed, 38 insertions(+) create mode 100644 tests/sandbox/test_sandbox_console.py create mode 100644 tests/sandbox/test_sandbox_console.test_goal_console.approved.txt diff --git a/src/algokit/cli/sandbox.py b/src/algokit/cli/sandbox.py index 7ec4613b..ded36fd8 100644 --- a/src/algokit/cli/sandbox.py +++ b/src/algokit/cli/sandbox.py @@ -2,6 +2,7 @@ import logging import click +from algokit.cli.goal import goal_command from algokit.core import exec from algokit.core.sandbox import ComposeFileStatus, ComposeSandbox, fetch_algod_status_data, fetch_indexer_status_data @@ -128,3 +129,13 @@ def sandbox_status() -> None: raise click.ClickException( "At least one container isn't running; execute `algokit sandbox start` to start the Sandbox" ) + + +@sandbox_group.command( + "console", + short_help="Run the Algorand goal CLI against the AlgoKit Sandbox via a Bash console" + + " so you can execute multiple goal commands and/or interact with a filesystem", +) +@click.pass_context +def sandbox_console(context: click.Context) -> None: + context.invoke(goal_command, console=True) diff --git a/tests/sandbox/test_sandbox_console.py b/tests/sandbox/test_sandbox_console.py new file mode 100644 index 00000000..551f146c --- /dev/null +++ b/tests/sandbox/test_sandbox_console.py @@ -0,0 +1,17 @@ +from subprocess import CompletedProcess + +from approvaltests import verify # type: ignore +from pytest_mock import MockerFixture +from utils.click_invoker import invoke +from utils.exec_mock import ExecMock + + +def test_goal_console(exec_mock: ExecMock, mocker: MockerFixture): + mocker.patch("algokit.core.exec.subprocess_run").return_value = CompletedProcess( + ["docker", "exec"], 0, "STDOUT+STDERR" + ) + + result = invoke("sandbox console") + + assert result.exit_code == 0 + verify(result.output) diff --git a/tests/sandbox/test_sandbox_console.test_goal_console.approved.txt b/tests/sandbox/test_sandbox_console.test_goal_console.approved.txt new file mode 100644 index 00000000..647ac70b --- /dev/null +++ b/tests/sandbox/test_sandbox_console.test_goal_console.approved.txt @@ -0,0 +1,10 @@ +DEBUG: Running 'docker compose version --format json' in '{current_working_directory}' +DEBUG: docker: {"version": "v2.5.0"} +DEBUG: Running 'docker version' in '{current_working_directory}' +DEBUG: docker: STDOUT +DEBUG: docker: STDERR +DEBUG: Running 'docker version' in '{current_working_directory}' +DEBUG: docker: STDOUT +DEBUG: docker: STDERR +Opening Bash console on the algod node; execute `exit` to return to original console +DEBUG: Running 'docker exec -it -w /root algokit_algod bash' in '{current_working_directory}'