This is an automated email from the ASF dual-hosted git repository.

henry3260 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new f31b572df18 Fix airflowctl commands crashing instead of printing their 
result (#72675)
f31b572df18 is described below

commit f31b572df18639acf75781aeae8628732e6e2eab
Author: Y-C <[email protected]>
AuthorDate: Thu Oct 1 03:57:26 2026 +0800

    Fix airflowctl commands crashing instead of printing their result (#72675)
    
    airflowctl generates its commands from the operations layer, and every
    generated command finishes by printing through ``args.output``. That flag
    was only declared for commands whose method name happened to start with
    one of ten CRUD verbs, so the ones that did not — ``assets materialize``,
    ``backfill pause|unpause|cancel`` and ``connections test`` — reached the
    printer with the attribute undefined and died with a raw traceback, after
    their request had already been sent and applied server-side.
    
    ``-e/--env`` deliberately keeps its existing whitelist. Nothing reads
    ``args.env`` for generated commands, so widening it would only let more
    commands silently accept an environment they then ignore and run against
    production credentials instead; that is tracked separately in
    https://github.com/apache/airflow/issues/70519.
    
    Co-authored-by: Eason09053360 
<[email protected]>
---
 airflow-ctl/src/airflowctl/ctl/cli_config.py       | 12 ++++----
 .../tests/airflow_ctl/ctl/test_cli_config.py       | 34 ++++++++++++++++++++--
 2 files changed, 39 insertions(+), 7 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 3379d4d0520..a4d6afd35e4 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -494,7 +494,7 @@ class CommandFactory:
     func_map: dict[tuple, Callable]
     commands_map: dict[str, list[ActionCommand]]
     group_commands_list: list[CLICommand]
-    output_command_list: list[str]
+    auth_environment_command_list: list[str]
     exclude_operation_names: list[str]
     exclude_method_names: list[str]
     help_texts: dict[str, dict[str, str]]
@@ -511,8 +511,7 @@ class CommandFactory:
         # Excluded Lists are in Class Level for further usage and avoid 
searching them
         # Exclude parameters that are not needed for CLI from datamodels
         self.excluded_parameters = ["schema_"]
-        # This list is used to determine if the command/operation needs to 
output data
-        self.output_command_list = [
+        self.auth_environment_command_list = [
             "list",
             "get",
             "create",
@@ -797,8 +796,11 @@ class CommandFactory:
                             )
                         )
 
-            if any(operation.get("name").startswith(cmd) for cmd in 
self.output_command_list):
-                args.extend([ARG_OUTPUT, ARG_AUTH_ENVIRONMENT])
+            args.append(ARG_OUTPUT)
+            # ``-e/--env`` is inert here (nothing reads ``args.env``), so 
widening it would only let more
+            # commands accept it and silently target production: 
https://github.com/apache/airflow/issues/70519
+            if any(operation.get("name").startswith(cmd) for cmd in 
self.auth_environment_command_list):
+                args.append(ARG_AUTH_ENVIRONMENT)
 
             self.args_map[(operation.get("name"), 
operation.get("parent").name)] = args
 
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py 
b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
index 45d55f08034..e4a7827592d 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -26,10 +26,13 @@ from unittest import mock
 import httpx
 import pytest
 
-from airflowctl.api.datamodels.generated import ClearTaskInstancesBody
-from airflowctl.api.operations import DagRunOperations, ServerResponseError
+from airflowctl.api.client import Client
+from airflowctl.api.datamodels.generated import ClearTaskInstancesBody, 
ConnectionTestResponse
+from airflowctl.api.operations import ConnectionsOperations, DagRunOperations, 
ServerResponseError
+from airflowctl.ctl import cli_parser
 from airflowctl.ctl.cli_config import (
     ARG_AUTH_TOKEN,
+    ARG_OUTPUT,
     ActionCommand,
     Arg,
     CommandFactory,
@@ -39,6 +42,7 @@ from airflowctl.ctl.cli_config import (
     merge_commands,
     safe_call_command,
 )
+from airflowctl.ctl.console_formatting import AirflowConsole
 from airflowctl.exceptions import (
     AirflowCtlConnectionException,
     AirflowCtlCredentialNotFoundException,
@@ -477,6 +481,19 @@ class TestCommandFactory:
         assert limit_arg.flags == ("--limit",)
         assert limit_arg.kwargs["type"] is int
 
+    def test_every_generated_command_accepts_the_output_flag(self):
+        """``_get_func`` always prints through ``args.output``, so every 
generated command must declare it."""
+        command_factory = CommandFactory()
+
+        missing = [
+            f"{group_command.name} {sub_command.name}"
+            for group_command in command_factory.group_commands
+            for sub_command in group_command.subcommands
+            if ARG_OUTPUT not in sub_command.args
+        ]
+
+        assert missing == []
+
 
 class TestCliConfigMethods:
     @pytest.mark.parametrize(
@@ -957,3 +974,16 @@ class TestCliConfigMethods:
         call_kwargs = self._call_generated_command(monkeypatch, 
DagRunOperations, "list")
 
         assert call_kwargs["state"] is None
+
+    @mock.patch.object(AirflowConsole, "print_as", autospec=True)
+    @mock.patch.object(ConnectionsOperations, "test", autospec=True)
+    def test_connections_test_reaches_the_printer(self, mocked_test, 
mocked_print_as):
+        """``connections test`` has no CRUD-verb prefix, so argparse used to 
leave ``args.output`` undefined."""
+        mocked_test.return_value = ConnectionTestResponse(status=True, 
message="ok")
+        args = cli_parser.get_parser().parse_args(
+            ["connections", "test", "--connection-id", "my_conn", 
"--conn-type", "http"]
+        )
+
+        args.func(args, api_client=mock.MagicMock(spec=Client))
+
+        assert mocked_print_as.call_args.kwargs["output"] == "json"

Reply via email to