|
2 | 2 | import logging
|
3 | 3 | import os
|
4 | 4 | import sys
|
| 5 | +import traceback |
5 | 6 | from typing import Dict, List, Optional
|
6 | 7 |
|
7 | 8 | from localstack import config
|
|
20 | 21 | from .plugin import LocalstackCli, load_cli_plugins
|
21 | 22 |
|
22 | 23 |
|
| 24 | +class ExceptionCmdHandler(click.Group): |
| 25 | + """ |
| 26 | + A Click group implementation which globally handles exceptions by: |
| 27 | + - Ignoring click exceptions (already handled) |
| 28 | + - Handling common exceptions (like DockerNotAvailable) |
| 29 | + - Wrapping all unexpected exceptions in a ClickException (for a unified error message) |
| 30 | + """ |
| 31 | + |
| 32 | + def invoke(self, ctx: click.Context): |
| 33 | + try: |
| 34 | + return super(ExceptionCmdHandler, self).invoke(ctx) |
| 35 | + except click.exceptions.Exit: |
| 36 | + # raise Exit exceptions unmodified (e.g., raised on --help) |
| 37 | + raise |
| 38 | + except click.ClickException: |
| 39 | + # don't handle ClickExceptions, just reraise |
| 40 | + if ctx and ctx.params.get("debug"): |
| 41 | + click.echo(traceback.format_exc()) |
| 42 | + raise |
| 43 | + except Exception as e: |
| 44 | + if ctx and ctx.params.get("debug"): |
| 45 | + click.echo(traceback.format_exc()) |
| 46 | + from localstack.utils.container_utils.container_client import ( |
| 47 | + ContainerException, |
| 48 | + DockerNotAvailable, |
| 49 | + ) |
| 50 | + |
| 51 | + if isinstance(e, DockerNotAvailable): |
| 52 | + raise click.ClickException( |
| 53 | + "Docker could not be found on the system.\n" |
| 54 | + "Please make sure that you have a working docker environment on your machine." |
| 55 | + ) |
| 56 | + elif isinstance(e, ContainerException): |
| 57 | + raise click.ClickException(e.message) |
| 58 | + else: |
| 59 | + # If we have a generic exception, we wrap it in a ClickException |
| 60 | + raise click.ClickException(str(e)) from e |
| 61 | + |
| 62 | + |
23 | 63 | def create_with_plugins() -> LocalstackCli:
|
24 | 64 | """
|
25 | 65 | Creates a LocalstackCli instance with all cli plugins loaded.
|
@@ -54,6 +94,7 @@ def _setup_cli_debug() -> None:
|
54 | 94 | @click.group(
|
55 | 95 | name="localstack",
|
56 | 96 | help="The LocalStack Command Line Interface (CLI)",
|
| 97 | + cls=ExceptionCmdHandler, |
57 | 98 | context_settings={
|
58 | 99 | # add "-h" as a synonym for "--help"
|
59 | 100 | # https://click.palletsprojects.com/en/8.1.x/documentation/#help-parameter-customization
|
@@ -149,19 +190,13 @@ def cmd_config_validate(file: str) -> None:
|
149 | 190 | - The docker-compose file is syntactically incorrect.
|
150 | 191 | - If the file contains common issues when configuring LocalStack.
|
151 | 192 | """
|
152 |
| - from rich.panel import Panel |
153 | 193 |
|
154 | 194 | from localstack.utils import bootstrap
|
155 | 195 |
|
156 |
| - try: |
157 |
| - if bootstrap.validate_localstack_config(file): |
158 |
| - console.print("[green]:heavy_check_mark:[/green] config valid") |
159 |
| - sys.exit(0) |
160 |
| - else: |
161 |
| - console.print("[red]:heavy_multiplication_x:[/red] validation error") |
162 |
| - sys.exit(1) |
163 |
| - except Exception as e: |
164 |
| - console.print(Panel(str(e), title="[red]Error[/red]", expand=False)) |
| 196 | + if bootstrap.validate_localstack_config(file): |
| 197 | + console.print("[green]:heavy_check_mark:[/green] config valid") |
| 198 | + sys.exit(0) |
| 199 | + else: |
165 | 200 | console.print("[red]:heavy_multiplication_x:[/red] validation error")
|
166 | 201 | sys.exit(1)
|
167 | 202 |
|
@@ -307,11 +342,9 @@ def cmd_status_services(format_: str) -> None:
|
307 | 342 | if format_ == "json":
|
308 | 343 | console.print(json.dumps(services))
|
309 | 344 | except requests.ConnectionError:
|
310 |
| - error = f"could not connect to LocalStack health endpoint at {url}" |
311 |
| - print_error(format_, error) |
312 | 345 | if config.DEBUG:
|
313 | 346 | console.print_exception()
|
314 |
| - sys.exit(1) |
| 347 | + raise click.ClickException(f"could not connect to LocalStack health endpoint at {url}") |
315 | 348 |
|
316 | 349 |
|
317 | 350 | def _print_service_table(services: Dict[str, str]) -> None:
|
@@ -431,8 +464,9 @@ def cmd_stop() -> None:
|
431 | 464 | DOCKER_CLIENT.stop_container(container_name)
|
432 | 465 | console.print("container stopped: %s" % container_name)
|
433 | 466 | except NoSuchContainer:
|
434 |
| - console.print("no such container: %s" % container_name) |
435 |
| - sys.exit(1) |
| 467 | + raise click.ClickException( |
| 468 | + f'Expected a running LocalStack container named "{container_name}", but found none' |
| 469 | + ) |
436 | 470 |
|
437 | 471 |
|
438 | 472 | @localstack.command(
|
@@ -539,7 +573,7 @@ def cmd_ssh() -> None:
|
539 | 573 |
|
540 | 574 | if not DOCKER_CLIENT.is_container_running(config.MAIN_CONTAINER_NAME):
|
541 | 575 | raise click.ClickException(
|
542 |
| - 'Expected a running container named "%s", but found none' % config.MAIN_CONTAINER_NAME |
| 576 | + f'Expected a running LocalStack container named "{config.MAIN_CONTAINER_NAME}", but found none' |
543 | 577 | )
|
544 | 578 | try:
|
545 | 579 | process = run("docker exec -it %s bash" % config.MAIN_CONTAINER_NAME, tty=True)
|
@@ -585,12 +619,10 @@ def cmd_update_localstack_cli() -> None:
|
585 | 619 | """
|
586 | 620 | if is_frozen_bundle():
|
587 | 621 | # "update" can only be performed if running from source / in a non-frozen interpreter
|
588 |
| - console.print( |
589 |
| - ":heavy_multiplication_x: The LocalStack CLI can only update itself if installed via PIP. " |
590 |
| - "Please follow the instructions on https://docs.localstack.cloud/ to update your CLI.", |
591 |
| - style="bold red", |
| 622 | + raise click.ClickException( |
| 623 | + "The LocalStack CLI can only update itself if installed via PIP. " |
| 624 | + "Please follow the instructions on https://docs.localstack.cloud/ to update your CLI." |
592 | 625 | )
|
593 |
| - sys.exit(1) |
594 | 626 |
|
595 | 627 | import subprocess
|
596 | 628 | from subprocess import CalledProcessError
|
@@ -770,18 +802,6 @@ def print_profile() -> None:
|
770 | 802 | )
|
771 | 803 |
|
772 | 804 |
|
773 |
| -def print_error(format_: str, error: str) -> None: |
774 |
| - if format_ == "table": |
775 |
| - symbol = "[bold][red]:heavy_multiplication_x: ERROR[/red][/bold]" |
776 |
| - console.print(f"{symbol}: {error}") |
777 |
| - if format_ == "plain": |
778 |
| - console.print(f"error={error}") |
779 |
| - if format_ == "dict": |
780 |
| - console.print({"error": error}) |
781 |
| - if format_ == "json": |
782 |
| - console.print(json.dumps({"error": error})) |
783 |
| - |
784 |
| - |
785 | 805 | def print_banner() -> None:
|
786 | 806 | print(BANNER)
|
787 | 807 |
|
|
0 commit comments