Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions airflow-ctl/src/airflowctl/ctl/cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import datetime
import inspect
import os
import sys
from argparse import Namespace
from collections.abc import Callable, Iterable
from enum import Enum
Expand Down Expand Up @@ -64,8 +65,6 @@ def command(*args, **kwargs):


def safe_call_command(function: Callable, args: Iterable[Arg]) -> None:
import sys

if os.getenv("AIRFLOW_CLI_DEBUG_MODE") == "true":
rich.print(
"[yellow]Debug mode is enabled. Please be aware that your credentials are not secure.\n"
Expand All @@ -90,10 +89,12 @@ def safe_call_command(function: Callable, args: Iterable[Arg]) -> None:
f"[red]Server response error: {e}. "
"Please check if the server is running and the API URL is correct.[/red]"
)
sys.exit(1)
except httpx.ReadTimeout as e:
rich.print(f"[red]Read timeout error: {e}[/red]")
if "timed out" in str(e):
rich.print("[red]Please check if the server is running and the API ready to accept calls.[/red]")
sys.exit(1)
except ServerResponseError as e:
rich.print(f"Server response error: {e}")
if "Client error message:" in str(e):
Expand All @@ -102,6 +103,7 @@ def safe_call_command(function: Callable, args: Iterable[Arg]) -> None:
"Please check the command and its parameters. "
"If you need help, run the command with --help."
)
sys.exit(1)


class DefaultHelpParser(argparse.ArgumentParser):
Expand Down
66 changes: 66 additions & 0 deletions airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
from argparse import BooleanOptionalAction
from textwrap import dedent

import httpx
import pytest

from airflowctl.api.operations import ServerResponseError
from airflowctl.ctl.cli_config import (
ARG_AUTH_TOKEN,
ActionCommand,
Expand All @@ -31,6 +33,13 @@
GroupCommand,
add_auth_token_to_all_commands,
merge_commands,
safe_call_command,
)
from airflowctl.exceptions import (
AirflowCtlConnectionException,
AirflowCtlCredentialNotFoundException,
AirflowCtlKeyringException,
AirflowCtlNotFoundException,
)


Expand Down Expand Up @@ -289,6 +298,63 @@ def delete(self, backfill_id: str) -> ServerResponseError | None:


class TestCliConfigMethods:
@pytest.mark.parametrize(
"raised_exception",
[
AirflowCtlCredentialNotFoundException("missing credentials"),
AirflowCtlConnectionException("connection failed"),
AirflowCtlKeyringException("keyring failure"),
AirflowCtlNotFoundException("resource not found"),
],
ids=["credential-not-found", "connection-error", "keyring-error", "not-found"],
)
def test_safe_call_command_exits_non_zero_for_airflowctl_exceptions(self, raised_exception):
def raise_error(_args):
raise raised_exception

with pytest.raises(SystemExit) as ctx:
safe_call_command(raise_error, args=argparse.Namespace())

assert ctx.value.code == 1

@pytest.mark.parametrize(
"raised_exception",
[
httpx.RemoteProtocolError("remote protocol error"),
httpx.ReadError("read error"),
],
ids=["remote-protocol-error", "read-error"],
)
def test_safe_call_command_exits_non_zero_for_httpx_protocol_errors(self, raised_exception):
def raise_error(_args):
raise raised_exception

with pytest.raises(SystemExit) as ctx:
safe_call_command(raise_error, args=argparse.Namespace())

assert ctx.value.code == 1

def test_safe_call_command_exits_non_zero_for_httpx_read_timeout(self):
def raise_error(_args):
raise httpx.ReadTimeout("timed out")

with pytest.raises(SystemExit) as ctx:
safe_call_command(raise_error, args=argparse.Namespace())

assert ctx.value.code == 1

def test_safe_call_command_exits_non_zero_for_server_response_error(self):
request = httpx.Request("GET", "http://localhost:8080/api/v2/dags")
response = httpx.Response(500, request=request, json={"detail": "boom"})

def raise_error(_args):
raise ServerResponseError("server error", request=request, response=response)

with pytest.raises(SystemExit) as ctx:
safe_call_command(raise_error, args=argparse.Namespace())

assert ctx.value.code == 1

def test_add_to_parser_drops_type_for_boolean_optional_action(self):
"""Test add_to_parser removes type for BooleanOptionalAction."""
parser = argparse.ArgumentParser()
Expand Down
Loading