Skip to content

Commit

Permalink
Merge pull request #65 from algorandfoundation/goal-shell
Browse files Browse the repository at this point in the history
feat(goal): Added algokit goal --console / algokit sandbox console
  • Loading branch information
robdmoore authored Nov 30, 2022
2 parents 57a1359 + 95565df commit 630e106
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 14 deletions.
34 changes: 24 additions & 10 deletions src/algokit/cli/goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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`",
)
11 changes: 11 additions & 0 deletions src/algokit/cli/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
29 changes: 29 additions & 0 deletions src/algokit/core/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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="")
33 changes: 29 additions & 4 deletions tests/goal/test_goal.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand All @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions tests/goal/test_goal.test_goal_console.approved.txt
Original file line number Diff line number Diff line change
@@ -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}'
6 changes: 6 additions & 0 deletions tests/goal/test_goal.test_goal_console_failed.approved.txt
Original file line number Diff line number Diff line change
@@ -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`
17 changes: 17 additions & 0 deletions tests/sandbox/test_sandbox_console.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions tests/sandbox/test_sandbox_console.test_goal_console.approved.txt
Original file line number Diff line number Diff line change
@@ -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}'

0 comments on commit 630e106

Please sign in to comment.