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

turaga 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 15fce018c86 feat(airflowctl): support on headless environments (#62217)
15fce018c86 is described below

commit 15fce018c86616b3cf5767005bb77c1880e453f3
Author: Augusto Hidalgo <[email protected]>
AuthorDate: Thu Feb 26 18:03:25 2026 +0100

    feat(airflowctl): support on headless environments (#62217)
    
    We make all commands use the AIRFLOW_CLI_TOKEN environment variable or
    `api-token` flag if provided.
---
 .../tests/airflowctl_tests/conftest.py             |  22 +++-
 .../airflowctl_tests/test_airflowctl_commands.py   |  56 +++++++---
 .../test_config_sensitive_masking.py               |   4 +-
 airflow-ctl/README.md                              |   3 +-
 airflow-ctl/docs/howto/index.rst                   |  12 +-
 airflow-ctl/docs/images/command_hashes.txt         |   4 +-
 airflow-ctl/docs/images/output_auth_login.svg      | 122 +++++++++++----------
 airflow-ctl/docs/images/output_version.svg         |  88 ++++++++-------
 airflow-ctl/docs/installation/prerequisites.rst    |   6 +-
 airflow-ctl/docs/security.rst                      |   1 +
 airflow-ctl/src/airflowctl/api/client.py           |  42 ++++---
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  34 +++++-
 .../src/airflowctl/ctl/commands/auth_command.py    |   7 +-
 airflow-ctl/tests/airflow_ctl/api/test_client.py   |  47 ++++++++
 .../airflow_ctl/ctl/commands/test_auth_command.py  |  47 +++++++-
 .../airflow_ctl/ctl/commands/test_pool_command.py  |  14 +--
 .../tests/airflow_ctl/ctl/test_cli_config.py       |  72 +++++++++++-
 17 files changed, 427 insertions(+), 154 deletions(-)

diff --git a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py 
b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
index 9e6ef75b7fa..cbcea44ec36 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
@@ -21,6 +21,7 @@ import subprocess
 import sys
 
 import pytest
+import requests
 from python_on_whales import DockerClient, docker
 
 from airflowctl_tests import console
@@ -35,16 +36,31 @@ from airflowctl_tests.constants import (
 from tests_common.test_utils.fernet import generate_fernet_key_string
 
 
[email protected](scope="module")
+def api_token():
+    url = "http://localhost:8080/auth/token";
+    payload = {"username": "airflow", "password": "airflow"}
+    try:
+        response = requests.post(url, json=payload)
+        response.raise_for_status()
+        token = response.json().get("access_token")
+        if not token:
+            raise ValueError("Response did not contain an access_token")
+        return token
+    except requests.exceptions.RequestException as e:
+        pytest.fail(f"Failed to obtain token: {e}")
+
+
 @pytest.fixture
 def run_command():
     """Fixture that provides a helper to run airflowctl commands."""
 
-    def _run_command(command: str, skip_login: bool = False) -> str:
+    def _run_command(command: str, env_vars: dict, skip_login: bool = False) 
-> str:
         import os
         from subprocess import PIPE, STDOUT, Popen
 
         host_envs = os.environ.copy()
-        host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
+        host_envs.update(env_vars)
 
         command_from_config = f"airflowctl {command}"
 
@@ -231,8 +247,6 @@ def docker_compose_up(tmp_path_factory):
     dot_env_file = tmp_dir / ".env"
     dot_env_file.write_text(
         f"AIRFLOW_UID={os.getuid()}\n"
-        # To enable debug mode for airflowctl CLI
-        "AIRFLOW_CTL_CLI_DEBUG_MODE=true\n"
         # To enable config operations to work
         "AIRFLOW__API__EXPOSE_CONFIG=true\n"
     )
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index 228321ac7e8..3f89a7c9698 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -18,8 +18,6 @@ from __future__ import annotations
 
 import pytest
 
-from airflowctl_tests.constants import LOGIN_COMMAND
-
 
 def date_param():
     import random
@@ -44,11 +42,12 @@ def date_param():
     return random_dt.isoformat()
 
 
-ONE_DATE_PARAM = date_param()
+# Passing password via command line is insecure but acceptable for testing 
purposes
+# Please do not do this in production, it enables possibility of exposing your 
credentials
+LOGIN_COMMAND = "auth login --username airflow --password airflow"
+LOGIN_COMMAND_SKIP_KEYRING = "auth login --skip-keyring"
+LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
 TEST_COMMANDS = [
-    # Passing password via command line is insecure but acceptable for testing 
purposes
-    # Please do not do this in production, it enables possibility of exposing 
your credentials
-    LOGIN_COMMAND,
     # Assets commands
     "assets list",
     "assets get --asset-id=1",
@@ -80,22 +79,22 @@ TEST_COMMANDS = [
     "dags list-version --dag-id=example_bash_operator",
     "dags list-warning",
     # Order of trigger and pause/unpause is important for test stability 
because state checked
-    f"dags trigger --dag-id=example_bash_operator 
--logical-date={ONE_DATE_PARAM} --run-after={ONE_DATE_PARAM}",
+    "dags trigger --dag-id=example_bash_operator --logical-date={date_param} 
--run-after={date_param}",
     # Test trigger without logical-date (should default to now)
     "dags trigger --dag-id=example_bash_operator",
     "dags pause example_bash_operator",
     "dags unpause example_bash_operator",
     # DAG Run commands
-    f'dagrun get --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}"',
+    'dagrun get --dag-id=example_bash_operator 
--dag-run-id="manual__{date_param}"',
     "dags update --dag-id=example_bash_operator --no-is-paused",
     # DAG Run commands
     "dagrun list --dag-id example_bash_operator --state success --limit=1",
     # XCom commands - need a DAG run with completed tasks
-    f'xcom add --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key 
--value=\'{{"test": "value"}}\'',
-    f'xcom get --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key',
-    f'xcom list --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0',
-    f'xcom edit --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key 
--value=\'{{"updated": "value"}}\'',
-    f'xcom delete --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key',
+    'xcom add --dag-id=example_bash_operator 
--dag-run-id="manual__{date_param}" --task-id=runme_0 --key=test_xcom_key 
--value=\'{{"test": "value"}}\'',
+    'xcom get --dag-id=example_bash_operator 
--dag-run-id="manual__{date_param}" --task-id=runme_0 --key=test_xcom_key',
+    'xcom list --dag-id=example_bash_operator 
--dag-run-id="manual__{date_param}" --task-id=runme_0',
+    'xcom edit --dag-id=example_bash_operator 
--dag-run-id="manual__{date_param}" --task-id=runme_0 --key=test_xcom_key 
--value=\'{{"updated": "value"}}\'',
+    'xcom delete --dag-id=example_bash_operator 
--dag-run-id="manual__{date_param}" --task-id=runme_0 --key=test_xcom_key',
     # Jobs commands
     "jobs list",
     # Pools commands
@@ -124,11 +123,38 @@ TEST_COMMANDS = [
     "version --remote",
 ]
 
+DATE_PARAM_1 = date_param()
+DATE_PARAM_2 = date_param()
+TEST_COMMANDS_DEBUG_MODE = [LOGIN_COMMAND] + 
[test.format(date_param=DATE_PARAM_1) for test in TEST_COMMANDS]
+TEST_COMMANDS_SKIP_KEYRING = [LOGIN_COMMAND_SKIP_KEYRING] + [
+    test.format(date_param=DATE_PARAM_2) for test in TEST_COMMANDS
+]
+
 
 @pytest.mark.flaky(reruns=3, reruns_delay=1)
 @pytest.mark.parametrize(
-    "command", TEST_COMMANDS, ids=[" ".join(command.split(" ", 2)[:2]) for 
command in TEST_COMMANDS]
+    "command",
+    TEST_COMMANDS_DEBUG_MODE,
+    ids=[" ".join(command.split(" ", 2)[:2]) for command in 
TEST_COMMANDS_DEBUG_MODE],
 )
 def test_airflowctl_commands(command: str, run_command):
     """Test airflowctl commands using docker-compose environment."""
-    run_command(command)
+    env_vars = {"AIRFLOW_CLI_DEBUG_MODE": "true"}
+
+    run_command(command, env_vars, skip_login=True)
+
+
[email protected](reruns=3, reruns_delay=1)
[email protected](
+    "command",
+    TEST_COMMANDS_SKIP_KEYRING,
+    ids=[" ".join(command.split(" ", 2)[:2]) for command in 
TEST_COMMANDS_SKIP_KEYRING],
+)
+def test_airflowctl_commands_skip_keyring(command: str, api_token: str, 
run_command):
+    """Test airflowctl commands using docker-compose environment without using 
keyring."""
+    env_vars = {}
+    env_vars["AIRFLOW_CLI_TOKEN"] = api_token
+    env_vars["AIRFLOW_CLI_DEBUG_MODE"] = "false"
+    env_vars["AIRFLOW_CLI_ENVIRONMENT"] = "nokeyring"
+
+    run_command(command, env_vars, skip_login=True)
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
index 84719bb4cc3..776629da3a2 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
@@ -41,7 +41,9 @@ def test_config_sensitive_masking(command: str, run_command):
     Airflow API, sensitive values (like fernet_key, sql_alchemy_conn) appear 
masked
     as '< hidden >' and do not leak actual secret values.
     """
-    stdout_result = run_command(command)
+
+    env_vars = {"AIRFLOW_CLI_DEBUG_MODE": "true"}
+    stdout_result = run_command(command, env_vars)
 
     # CRITICAL: Verify that sensitive values are masked
     # The Airflow API returns masked values as "< hidden >" for sensitive 
configs
diff --git a/airflow-ctl/README.md b/airflow-ctl/README.md
index 28f1b473812..19e11b1369d 100644
--- a/airflow-ctl/README.md
+++ b/airflow-ctl/README.md
@@ -33,7 +33,8 @@ A command-line tool for interacting with Apache Airflow 
instances through the Ai
 
 - Python 3.10 or later (compatible with Python >= 3.10 and < 3.13)
 - Network access to an Apache Airflow instance with REST API enabled
-- Keyring backend installed in operating system for secure token storage
+- \[Recommended\] Keyring backend installed in operating system for secure 
token storage.
+  - In case there's no keyring available (common in headless environments) you 
can provide the token to each command. See the [Security 
page](https://airflow.apache.org/docs/apache-airflow-ctl/stable/security.html) 
for more information.
 
 ## Usage
 
diff --git a/airflow-ctl/docs/howto/index.rst b/airflow-ctl/docs/howto/index.rst
index 12946aed449..155dab5fc04 100644
--- a/airflow-ctl/docs/howto/index.rst
+++ b/airflow-ctl/docs/howto/index.rst
@@ -53,6 +53,7 @@ airflowctl needs to be able to connect to the Airflow API. 
You should pass API U
 You can also set the environment variable ``AIRFLOW_CLI_TOKEN`` to the token 
to use for authentication.
 
 There are two ways to authenticate with the Airflow API:
+
 1. Using a token acquired from the Airflow API
 
 .. code-block:: bash
@@ -61,17 +62,18 @@ There are two ways to authenticate with the Airflow API:
 
 2. Using a username and password
 
-
 .. code-block:: bash
 
   airflowctl auth login --api-url <api_url> --username <username> --password 
<password> --env <env_name:production>
 
-3. (optional) Using a token acquired from the Airflow API and username and 
password
+If there's no keyring available, common in headless systems like docker 
images, you can still use the tool by setting
+the environment variable ``AIRFLOW_CLI_TOKEN``.
 
 .. code-block:: bash
 
   export AIRFLOW_CLI_TOKEN=<token>
-  airflowctl auth login --api-url <api_url> --env <env_name>
+  airflowctl auth login --api-url <api_url> --env <env_name:production> 
--skip-keyring
+
 
 In both cases token is securely stored in the keyring backend. Only 
configuration persisted in ``~/.config/airflow`` file
 is the API URL and the environment name. The token is stored in the keyring 
backend and is not persisted in the
@@ -108,6 +110,10 @@ If you provide a username via ``--username`` this is the 
required password to au
 The name of the environment to create or update. The default value is 
``production``.
 This parameter is useful when you want to manage multiple Airflow environments.
 
+**--skip-keyring**: This parameter is optional.
+Useful when there's no keyring available in the system where airflowctl is 
running.
+Set ``AIRFLOW_CLI_TOKEN`` or use the ``--api-token`` flag for next commands.
+
 More Usage and Help Pictures
 ''''''''''''''''''''''''''''
 For more information use
diff --git a/airflow-ctl/docs/images/command_hashes.txt 
b/airflow-ctl/docs/images/command_hashes.txt
index 7f990f14b1b..46f6f42baf0 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -10,5 +10,5 @@ jobs:7f8680afff230eb9940bc7fca727bd52
 pools:03fc7d948cbecf16ff8d640eb8f0ce43
 providers:1c0afb2dff31d93ab2934b032a2250ab
 variables:0354f8f4b0dde1c3771ed1568692c6ae
-version:d4a7a6229b3a204f114283b62eac789b
-auth login:5277c653ff6dce51f37472dc0bda9775
+version:31f4efdf8de0dbaaa4fac71ff7efecc3
+auth login:f85e04072626ab4ae17ad17e4a077bf2
diff --git a/airflow-ctl/docs/images/output_auth_login.svg 
b/airflow-ctl/docs/images/output_auth_login.svg
index afa17cb9b56..006ed5bc266 100644
--- a/airflow-ctl/docs/images/output_auth_login.svg
+++ b/airflow-ctl/docs/images/output_auth_login.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 811 440.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 464.79999999999995" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,103 +19,107 @@
         font-weight: 700;
     }
 
-    .terminal-2480777825-matrix {
+    .terminal-938154658-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-2480777825-title {
+    .terminal-938154658-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-2480777825-r1 { fill: #ff8700 }
-.terminal-2480777825-r2 { fill: #c5c8c6 }
-.terminal-2480777825-r3 { fill: #808080 }
-.terminal-2480777825-r4 { fill: #68a0b3 }
-.terminal-2480777825-r5 { fill: #00af87 }
+    .terminal-938154658-r1 { fill: #ff8700 }
+.terminal-938154658-r2 { fill: #c5c8c6 }
+.terminal-938154658-r3 { fill: #808080 }
+.terminal-938154658-r4 { fill: #68a0b3 }
+.terminal-938154658-r5 { fill: #00af87 }
     </style>
 
     <defs>
-    <clipPath id="terminal-2480777825-clip-terminal">
-      <rect x="0" y="0" width="792.0" height="389.4" />
+    <clipPath id="terminal-938154658-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="413.79999999999995" />
     </clipPath>
-    <clipPath id="terminal-2480777825-line-0">
-    <rect x="0" y="1.5" width="793" height="24.65"/>
+    <clipPath id="terminal-938154658-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-1">
-    <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-2">
-    <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-3">
-    <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-4">
-    <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-5">
-    <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-6">
-    <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-7">
-    <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-8">
-    <rect x="0" y="196.7" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-8">
+    <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-9">
-    <rect x="0" y="221.1" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-9">
+    <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-10">
-    <rect x="0" y="245.5" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-10">
+    <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-11">
-    <rect x="0" y="269.9" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-11">
+    <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-12">
-    <rect x="0" y="294.3" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-12">
+    <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-13">
-    <rect x="0" y="318.7" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-13">
+    <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2480777825-line-14">
-    <rect x="0" y="343.1" width="793" height="24.65"/>
+<clipPath id="terminal-938154658-line-14">
+    <rect x="0" y="343.1" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-938154658-line-15">
+    <rect x="0" y="367.5" width="915" height="24.65"/>
             </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="809" height="438.4" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="462.8" rx="8"/>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
             <circle cx="44" cy="0" r="7" fill="#28c840"/>
             </g>
         
-    <g transform="translate(9, 41)" 
clip-path="url(#terminal-2480777825-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-938154658-clip-terminal)">
     
-    <g class="terminal-2480777825-matrix">
-    <text class="terminal-2480777825-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-2480777825-line-0)">Usage:</text><text 
class="terminal-2480777825-r3" x="85.4" y="20" textLength="256.2" 
clip-path="url(#terminal-2480777825-line-0)">airflowctl&#160;auth&#160;login</text><text
 class="terminal-2480777825-r2" x="341.6" y="20" textLength="24.4" 
clip-path="url(#terminal-2480777825-line-0)">&#160;[</text><text 
class="terminal-2480777825-r4" x="366" y="20" textLength="24.4" clip-p [...]
-</text><text class="terminal-2480777825-r2" x="0" y="44.4" textLength="366" 
clip-path="url(#terminal-2480777825-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-2480777825-r4" x="366" y="44.4" textLength="109.8" 
clip-path="url(#terminal-2480777825-line-1)">--api-url</text><text 
class="terminal-2480777825-r5" x="488" y="44.4" t [...]
-</text><text class="terminal-2480777825-r2" x="0" y="68.8" textLength="366" 
clip-path="url(#terminal-2480777825-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-2480777825-r4" x="366" y="68.8" textLength="122" 
clip-path="url(#terminal-2480777825-line-2)">--password</text><text 
class="terminal-2480777825-r2" x="488" y="68.8" te [...]
-</text><text class="terminal-2480777825-r2" x="0" y="93.2" textLength="366" 
clip-path="url(#terminal-2480777825-line-3)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-2480777825-r4" x="366" y="93.2" textLength="122" 
clip-path="url(#terminal-2480777825-line-3)">--username</text><text 
class="terminal-2480777825-r5" x="500.2" y="93.2"  [...]
-</text><text class="terminal-2480777825-r2" x="793" y="117.6" 
textLength="12.2" clip-path="url(#terminal-2480777825-line-4)">
-</text><text class="terminal-2480777825-r2" x="0" y="142" textLength="366" 
clip-path="url(#terminal-2480777825-line-5)">Login&#160;to&#160;the&#160;metadata&#160;database</text><text
 class="terminal-2480777825-r2" x="793" y="142" textLength="12.2" 
clip-path="url(#terminal-2480777825-line-5)">
-</text><text class="terminal-2480777825-r2" x="793" y="166.4" 
textLength="12.2" clip-path="url(#terminal-2480777825-line-6)">
-</text><text class="terminal-2480777825-r1" x="0" y="190.8" textLength="97.6" 
clip-path="url(#terminal-2480777825-line-7)">Options:</text><text 
class="terminal-2480777825-r2" x="793" y="190.8" textLength="12.2" 
clip-path="url(#terminal-2480777825-line-7)">
-</text><text class="terminal-2480777825-r4" x="24.4" y="215.2" 
textLength="24.4" clip-path="url(#terminal-2480777825-line-8)">-h</text><text 
class="terminal-2480777825-r2" x="48.8" y="215.2" textLength="24.4" 
clip-path="url(#terminal-2480777825-line-8)">,&#160;</text><text 
class="terminal-2480777825-r4" x="73.2" y="215.2" textLength="73.2" 
clip-path="url(#terminal-2480777825-line-8)">--help</text><text 
class="terminal-2480777825-r2" x="292.8" y="215.2" textLength="378.2" 
clip-path="url(# [...]
-</text><text class="terminal-2480777825-r4" x="24.4" y="239.6" 
textLength="134.2" 
clip-path="url(#terminal-2480777825-line-9)">--api-token</text><text 
class="terminal-2480777825-r5" x="170.8" y="239.6" textLength="109.8" 
clip-path="url(#terminal-2480777825-line-9)">API_TOKEN</text><text 
class="terminal-2480777825-r2" x="793" y="239.6" textLength="12.2" 
clip-path="url(#terminal-2480777825-line-9)">
-</text><text class="terminal-2480777825-r2" x="292.8" y="264" textLength="427" 
clip-path="url(#terminal-2480777825-line-10)">The&#160;token&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-2480777825-r2" x="793" y="264" textLength="12.2" 
clip-path="url(#terminal-2480777825-line-10)">
-</text><text class="terminal-2480777825-r4" x="24.4" y="288.4" 
textLength="109.8" 
clip-path="url(#terminal-2480777825-line-11)">--api-url</text><text 
class="terminal-2480777825-r5" x="146.4" y="288.4" textLength="85.4" 
clip-path="url(#terminal-2480777825-line-11)">API_URL</text><text 
class="terminal-2480777825-r2" x="292.8" y="288.4" textLength="439.2" 
clip-path="url(#terminal-2480777825-line-11)">The&#160;URL&#160;of&#160;the&#160;metadata&#160;database&#160;API</text><text
 class="termi [...]
-</text><text class="terminal-2480777825-r4" x="24.4" y="312.8" 
textLength="24.4" clip-path="url(#terminal-2480777825-line-12)">-e</text><text 
class="terminal-2480777825-r2" x="48.8" y="312.8" textLength="24.4" 
clip-path="url(#terminal-2480777825-line-12)">,&#160;</text><text 
class="terminal-2480777825-r4" x="73.2" y="312.8" textLength="61" 
clip-path="url(#terminal-2480777825-line-12)">--env</text><text 
class="terminal-2480777825-r5" x="146.4" y="312.8" textLength="36.6" 
clip-path="url(#t [...]
-</text><text class="terminal-2480777825-r4" x="24.4" y="337.2" 
textLength="122" 
clip-path="url(#terminal-2480777825-line-13)">--password</text><text 
class="terminal-2480777825-r2" x="146.4" y="337.2" textLength="24.4" 
clip-path="url(#terminal-2480777825-line-13)">&#160;[</text><text 
class="terminal-2480777825-r5" x="170.8" y="337.2" textLength="97.6" 
clip-path="url(#terminal-2480777825-line-13)">PASSWORD</text><text 
class="terminal-2480777825-r2" x="268.4" y="337.2" textLength="12.2" cli [...]
-</text><text class="terminal-2480777825-r2" x="292.8" y="361.6" 
textLength="463.6" 
clip-path="url(#terminal-2480777825-line-14)">The&#160;password&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-2480777825-r2" x="793" y="361.6" textLength="12.2" 
clip-path="url(#terminal-2480777825-line-14)">
-</text><text class="terminal-2480777825-r4" x="24.4" y="386" textLength="122" 
clip-path="url(#terminal-2480777825-line-15)">--username</text><text 
class="terminal-2480777825-r5" x="158.6" y="386" textLength="97.6" 
clip-path="url(#terminal-2480777825-line-15)">USERNAME</text><text 
class="terminal-2480777825-r2" x="292.8" y="386" textLength="463.6" 
clip-path="url(#terminal-2480777825-line-15)">The&#160;username&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-24807 [...]
+    <g class="terminal-938154658-matrix">
+    <text class="terminal-938154658-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-938154658-line-0)">Usage:</text><text 
class="terminal-938154658-r3" x="85.4" y="20" textLength="256.2" 
clip-path="url(#terminal-938154658-line-0)">airflowctl&#160;auth&#160;login</text><text
 class="terminal-938154658-r2" x="341.6" y="20" textLength="24.4" 
clip-path="url(#terminal-938154658-line-0)">&#160;[</text><text 
class="terminal-938154658-r4" x="366" y="20" textLength="24.4" clip-path="ur 
[...]
+</text><text class="terminal-938154658-r2" x="0" y="44.4" textLength="366" 
clip-path="url(#terminal-938154658-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-938154658-r4" x="366" y="44.4" textLength="109.8" 
clip-path="url(#terminal-938154658-line-1)">--api-url</text><text 
class="terminal-938154658-r5" x="488" y="44.4" textLe [...]
+</text><text class="terminal-938154658-r2" x="0" y="68.8" textLength="366" 
clip-path="url(#terminal-938154658-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-938154658-r4" x="366" y="68.8" textLength="122" 
clip-path="url(#terminal-938154658-line-2)">--password</text><text 
class="terminal-938154658-r2" x="488" y="68.8" textLen [...]
+</text><text class="terminal-938154658-r2" x="0" y="93.2" textLength="366" 
clip-path="url(#terminal-938154658-line-3)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-938154658-r4" x="366" y="93.2" textLength="122" 
clip-path="url(#terminal-938154658-line-3)">--username</text><text 
class="terminal-938154658-r5" x="500.2" y="93.2" textL [...]
+</text><text class="terminal-938154658-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-938154658-line-4)">
+</text><text class="terminal-938154658-r2" x="0" y="142" textLength="366" 
clip-path="url(#terminal-938154658-line-5)">Login&#160;to&#160;the&#160;metadata&#160;database</text><text
 class="terminal-938154658-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-938154658-line-5)">
+</text><text class="terminal-938154658-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-938154658-line-6)">
+</text><text class="terminal-938154658-r1" x="0" y="190.8" textLength="97.6" 
clip-path="url(#terminal-938154658-line-7)">Options:</text><text 
class="terminal-938154658-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-938154658-line-7)">
+</text><text class="terminal-938154658-r4" x="24.4" y="215.2" 
textLength="24.4" clip-path="url(#terminal-938154658-line-8)">-h</text><text 
class="terminal-938154658-r2" x="48.8" y="215.2" textLength="24.4" 
clip-path="url(#terminal-938154658-line-8)">,&#160;</text><text 
class="terminal-938154658-r4" x="73.2" y="215.2" textLength="73.2" 
clip-path="url(#terminal-938154658-line-8)">--help</text><text 
class="terminal-938154658-r2" x="292.8" y="215.2" textLength="378.2" 
clip-path="url(#termina [...]
+</text><text class="terminal-938154658-r4" x="24.4" y="239.6" 
textLength="134.2" 
clip-path="url(#terminal-938154658-line-9)">--api-token</text><text 
class="terminal-938154658-r5" x="170.8" y="239.6" textLength="109.8" 
clip-path="url(#terminal-938154658-line-9)">API_TOKEN</text><text 
class="terminal-938154658-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-938154658-line-9)">
+</text><text class="terminal-938154658-r2" x="292.8" y="264" textLength="427" 
clip-path="url(#terminal-938154658-line-10)">The&#160;token&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-938154658-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-938154658-line-10)">
+</text><text class="terminal-938154658-r4" x="24.4" y="288.4" 
textLength="109.8" 
clip-path="url(#terminal-938154658-line-11)">--api-url</text><text 
class="terminal-938154658-r5" x="146.4" y="288.4" textLength="85.4" 
clip-path="url(#terminal-938154658-line-11)">API_URL</text><text 
class="terminal-938154658-r2" x="292.8" y="288.4" textLength="439.2" 
clip-path="url(#terminal-938154658-line-11)">The&#160;URL&#160;of&#160;the&#160;metadata&#160;database&#160;API</text><text
 class="terminal-93 [...]
+</text><text class="terminal-938154658-r4" x="24.4" y="312.8" 
textLength="24.4" clip-path="url(#terminal-938154658-line-12)">-e</text><text 
class="terminal-938154658-r2" x="48.8" y="312.8" textLength="24.4" 
clip-path="url(#terminal-938154658-line-12)">,&#160;</text><text 
class="terminal-938154658-r4" x="73.2" y="312.8" textLength="61" 
clip-path="url(#terminal-938154658-line-12)">--env</text><text 
class="terminal-938154658-r5" x="146.4" y="312.8" textLength="36.6" 
clip-path="url(#terminal [...]
+</text><text class="terminal-938154658-r4" x="24.4" y="337.2" textLength="122" 
clip-path="url(#terminal-938154658-line-13)">--password</text><text 
class="terminal-938154658-r2" x="146.4" y="337.2" textLength="24.4" 
clip-path="url(#terminal-938154658-line-13)">&#160;[</text><text 
class="terminal-938154658-r5" x="170.8" y="337.2" textLength="97.6" 
clip-path="url(#terminal-938154658-line-13)">PASSWORD</text><text 
class="terminal-938154658-r2" x="268.4" y="337.2" textLength="12.2" clip-path= 
[...]
+</text><text class="terminal-938154658-r2" x="292.8" y="361.6" 
textLength="463.6" 
clip-path="url(#terminal-938154658-line-14)">The&#160;password&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-938154658-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-938154658-line-14)">
+</text><text class="terminal-938154658-r4" x="24.4" y="386" textLength="170.8" 
clip-path="url(#terminal-938154658-line-15)">--skip-keyring</text><text 
class="terminal-938154658-r2" x="292.8" y="386" textLength="427" 
clip-path="url(#terminal-938154658-line-15)">Skip&#160;storing&#160;credentials&#160;in&#160;keyring</text><text
 class="terminal-938154658-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-938154658-line-15)">
+</text><text class="terminal-938154658-r4" x="24.4" y="410.4" textLength="122" 
clip-path="url(#terminal-938154658-line-16)">--username</text><text 
class="terminal-938154658-r5" x="158.6" y="410.4" textLength="97.6" 
clip-path="url(#terminal-938154658-line-16)">USERNAME</text><text 
class="terminal-938154658-r2" x="292.8" y="410.4" textLength="463.6" 
clip-path="url(#terminal-938154658-line-16)">The&#160;username&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-93815 [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/docs/images/output_version.svg 
b/airflow-ctl/docs/images/output_version.svg
index a7ce436c8b8..ade6b57810e 100644
--- a/airflow-ctl/docs/images/output_version.svg
+++ b/airflow-ctl/docs/images/output_version.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 811 269.6" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 342.79999999999995" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,75 +19,87 @@
         font-weight: 700;
     }
 
-    .terminal-3399644434-matrix {
+    .terminal-1180839071-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-3399644434-title {
+    .terminal-1180839071-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-3399644434-r1 { fill: #ff8700 }
-.terminal-3399644434-r2 { fill: #c5c8c6 }
-.terminal-3399644434-r3 { fill: #808080 }
-.terminal-3399644434-r4 { fill: #68a0b3 }
-.terminal-3399644434-r5 { fill: #00af87 }
+    .terminal-1180839071-r1 { fill: #ff8700 }
+.terminal-1180839071-r2 { fill: #c5c8c6 }
+.terminal-1180839071-r3 { fill: #808080 }
+.terminal-1180839071-r4 { fill: #68a0b3 }
+.terminal-1180839071-r5 { fill: #00af87 }
     </style>
 
     <defs>
-    <clipPath id="terminal-3399644434-clip-terminal">
-      <rect x="0" y="0" width="792.0" height="218.6" />
+    <clipPath id="terminal-1180839071-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="291.79999999999995" />
     </clipPath>
-    <clipPath id="terminal-3399644434-line-0">
-    <rect x="0" y="1.5" width="793" height="24.65"/>
+    <clipPath id="terminal-1180839071-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-1">
-    <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-2">
-    <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-3">
-    <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-4">
-    <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-5">
-    <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-6">
-    <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3399644434-line-7">
-    <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-1180839071-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1180839071-line-8">
+    <rect x="0" y="196.7" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1180839071-line-9">
+    <rect x="0" y="221.1" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1180839071-line-10">
+    <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="809" height="267.6" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="340.8" rx="8"/>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
             <circle cx="44" cy="0" r="7" fill="#28c840"/>
             </g>
         
-    <g transform="translate(9, 41)" 
clip-path="url(#terminal-3399644434-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-1180839071-clip-terminal)">
     
-    <g class="terminal-3399644434-matrix">
-    <text class="terminal-3399644434-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-3399644434-line-0)">Usage:</text><text 
class="terminal-3399644434-r3" x="85.4" y="20" textLength="219.6" 
clip-path="url(#terminal-3399644434-line-0)">airflowctl&#160;version</text><text
 class="terminal-3399644434-r2" x="305" y="20" textLength="24.4" 
clip-path="url(#terminal-3399644434-line-0)">&#160;[</text><text 
class="terminal-3399644434-r4" x="329.4" y="20" textLength="24.4" 
clip-path="url [...]
-</text><text class="terminal-3399644434-r2" x="793" y="44.4" textLength="12.2" 
clip-path="url(#terminal-3399644434-line-1)">
-</text><text class="terminal-3399644434-r2" x="0" y="68.8" textLength="292.8" 
clip-path="url(#terminal-3399644434-line-2)">Show&#160;version&#160;information</text><text
 class="terminal-3399644434-r2" x="793" y="68.8" textLength="12.2" 
clip-path="url(#terminal-3399644434-line-2)">
-</text><text class="terminal-3399644434-r2" x="793" y="93.2" textLength="12.2" 
clip-path="url(#terminal-3399644434-line-3)">
-</text><text class="terminal-3399644434-r1" x="0" y="117.6" textLength="97.6" 
clip-path="url(#terminal-3399644434-line-4)">Options:</text><text 
class="terminal-3399644434-r2" x="793" y="117.6" textLength="12.2" 
clip-path="url(#terminal-3399644434-line-4)">
-</text><text class="terminal-3399644434-r4" x="24.4" y="142" textLength="24.4" 
clip-path="url(#terminal-3399644434-line-5)">-h</text><text 
class="terminal-3399644434-r2" x="48.8" y="142" textLength="24.4" 
clip-path="url(#terminal-3399644434-line-5)">,&#160;</text><text 
class="terminal-3399644434-r4" x="73.2" y="142" textLength="73.2" 
clip-path="url(#terminal-3399644434-line-5)">--help</text><text 
class="terminal-3399644434-r2" x="256.2" y="142" textLength="378.2" 
clip-path="url(#terminal [...]
-</text><text class="terminal-3399644434-r4" x="24.4" y="166.4" 
textLength="24.4" clip-path="url(#terminal-3399644434-line-6)">-e</text><text 
class="terminal-3399644434-r2" x="48.8" y="166.4" textLength="24.4" 
clip-path="url(#terminal-3399644434-line-6)">,&#160;</text><text 
class="terminal-3399644434-r4" x="73.2" y="166.4" textLength="61" 
clip-path="url(#terminal-3399644434-line-6)">--env</text><text 
class="terminal-3399644434-r5" x="146.4" y="166.4" textLength="36.6" 
clip-path="url(#term [...]
-</text><text class="terminal-3399644434-r4" x="24.4" y="190.8" 
textLength="97.6" 
clip-path="url(#terminal-3399644434-line-7)">--remote</text><text 
class="terminal-3399644434-r2" x="256.2" y="190.8" textLength="536.8" 
clip-path="url(#terminal-3399644434-line-7)">Fetch&#160;the&#160;Airflow&#160;version&#160;in&#160;remote&#160;server,&#160;</text><text
 class="terminal-3399644434-r2" x="793" y="190.8" textLength="12.2" 
clip-path="url(#terminal-3399644434-line-7)">
-</text><text class="terminal-3399644434-r2" x="0" y="215.2" textLength="597.8" 
clip-path="url(#terminal-3399644434-line-8)">otherwise&#160;only&#160;shows&#160;the&#160;local&#160;airflowctl&#160;version</text><text
 class="terminal-3399644434-r2" x="793" y="215.2" textLength="12.2" 
clip-path="url(#terminal-3399644434-line-8)">
+    <g class="terminal-1180839071-matrix">
+    <text class="terminal-1180839071-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-1180839071-line-0)">Usage:</text><text 
class="terminal-1180839071-r3" x="85.4" y="20" textLength="219.6" 
clip-path="url(#terminal-1180839071-line-0)">airflowctl&#160;version</text><text
 class="terminal-1180839071-r2" x="305" y="20" textLength="24.4" 
clip-path="url(#terminal-1180839071-line-0)">&#160;[</text><text 
class="terminal-1180839071-r4" x="329.4" y="20" textLength="24.4" 
clip-path="url [...]
+</text><text class="terminal-1180839071-r2" x="0" y="44.4" textLength="329.4" 
clip-path="url(#terminal-1180839071-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text
 class="terminal-1180839071-r4" x="329.4" y="44.4" textLength="97.6" 
clip-path="url(#terminal-1180839071-line-1)">--remote</text><text 
class="terminal-1180839071-r2" x="427" y="44.4" textLength="12.2" [...]
+</text><text class="terminal-1180839071-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-2)">
+</text><text class="terminal-1180839071-r2" x="0" y="93.2" textLength="292.8" 
clip-path="url(#terminal-1180839071-line-3)">Show&#160;version&#160;information</text><text
 class="terminal-1180839071-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-3)">
+</text><text class="terminal-1180839071-r2" x="915" y="117.6" 
textLength="12.2" clip-path="url(#terminal-1180839071-line-4)">
+</text><text class="terminal-1180839071-r1" x="0" y="142" textLength="97.6" 
clip-path="url(#terminal-1180839071-line-5)">Options:</text><text 
class="terminal-1180839071-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-5)">
+</text><text class="terminal-1180839071-r4" x="24.4" y="166.4" 
textLength="24.4" clip-path="url(#terminal-1180839071-line-6)">-h</text><text 
class="terminal-1180839071-r2" x="48.8" y="166.4" textLength="24.4" 
clip-path="url(#terminal-1180839071-line-6)">,&#160;</text><text 
class="terminal-1180839071-r4" x="73.2" y="166.4" textLength="73.2" 
clip-path="url(#terminal-1180839071-line-6)">--help</text><text 
class="terminal-1180839071-r2" x="292.8" y="166.4" textLength="378.2" 
clip-path="url(# [...]
+</text><text class="terminal-1180839071-r4" x="24.4" y="190.8" 
textLength="134.2" 
clip-path="url(#terminal-1180839071-line-7)">--api-token</text><text 
class="terminal-1180839071-r5" x="170.8" y="190.8" textLength="109.8" 
clip-path="url(#terminal-1180839071-line-7)">API_TOKEN</text><text 
class="terminal-1180839071-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-7)">
+</text><text class="terminal-1180839071-r2" x="292.8" y="215.2" 
textLength="427" 
clip-path="url(#terminal-1180839071-line-8)">The&#160;token&#160;to&#160;use&#160;for&#160;authentication</text><text
 class="terminal-1180839071-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-8)">
+</text><text class="terminal-1180839071-r4" x="24.4" y="239.6" 
textLength="24.4" clip-path="url(#terminal-1180839071-line-9)">-e</text><text 
class="terminal-1180839071-r2" x="48.8" y="239.6" textLength="24.4" 
clip-path="url(#terminal-1180839071-line-9)">,&#160;</text><text 
class="terminal-1180839071-r4" x="73.2" y="239.6" textLength="61" 
clip-path="url(#terminal-1180839071-line-9)">--env</text><text 
class="terminal-1180839071-r5" x="146.4" y="239.6" textLength="36.6" 
clip-path="url(#term [...]
+</text><text class="terminal-1180839071-r4" x="24.4" y="264" textLength="97.6" 
clip-path="url(#terminal-1180839071-line-10)">--remote</text><text 
class="terminal-1180839071-r2" x="292.8" y="264" textLength="536.8" 
clip-path="url(#terminal-1180839071-line-10)">Fetch&#160;the&#160;Airflow&#160;version&#160;in&#160;remote&#160;server,&#160;</text><text
 class="terminal-1180839071-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-10)">
+</text><text class="terminal-1180839071-r2" x="0" y="288.4" textLength="597.8" 
clip-path="url(#terminal-1180839071-line-11)">otherwise&#160;only&#160;shows&#160;the&#160;local&#160;airflowctl&#160;version</text><text
 class="terminal-1180839071-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-1180839071-line-11)">
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/docs/installation/prerequisites.rst 
b/airflow-ctl/docs/installation/prerequisites.rst
index a155a8debd8..b01ff4c3cc4 100644
--- a/airflow-ctl/docs/installation/prerequisites.rst
+++ b/airflow-ctl/docs/installation/prerequisites.rst
@@ -25,8 +25,8 @@ The minimum memory required we recommend airflowctl to run 
with is 200MB, but th
 wildly on the deployment options you have.
 The Keyring backend needs to be installed separately into your operating 
system. This will enhance security. See :doc:`/security` for more information.
 
-Keyring Backend
-'''''''''''''''
+Keyring Backend \[Recommended\]
+'''''''''''''''''''''''''''''''
 airflowctl uses keyring to store the API token securely. This ensures that the 
token is not stored in plain text and is only accessible to authorized users.
 
 Recommended keyring backends are:
@@ -36,6 +36,8 @@ Recommended keyring backends are:
 * `KDE4 & KDE5 KWallet <https://en.wikipedia.org/wiki/KWallet>`_ (requires 
`dbus <https://pypi.python.org/pypi/dbus-python>`_)
 * `Windows Credential Locker 
<https://docs.microsoft.com/en-us/windows/uwp/security/credential-locker>`_
 
+In case there's no keyring available (common in headless environments) you can 
provide the token to each command. See :doc:`/security` for more information.
+
 Third-Party Backends
 ====================
 
diff --git a/airflow-ctl/docs/security.rst b/airflow-ctl/docs/security.rst
index 8ec5ea6d6f4..fb08f4f68b3 100644
--- a/airflow-ctl/docs/security.rst
+++ b/airflow-ctl/docs/security.rst
@@ -23,6 +23,7 @@ airflowctl facilitates the seamless deployment of CLI and API 
features together,
 - **Authentication**: airflowctl uses authentication to ensure that only 
authorized users can access the system. This is done using an API Token. See 
more on https://airflow.apache.org/docs/apache-airflow/stable/security/api.html
 
 - **Keyring**: airflowctl uses keyring to store the API Token securely. This 
ensures that the Token is not stored in plain text and is only accessible to 
authorized users.
+   - In case no keyring is available, you can set the ``AIRFLOW_CLI_TOKEN`` 
environment variable or the ``--api-token`` flag for each command. Be cautious 
of not exposing this token to others.
 
 airflowctl API Token has its own expiration time. The default is 1 hour. You 
can change it in the Airflow configuration file (airflow.cfg) by setting the 
``jwt_cli_expiration_time`` parameter under the ``[api_auth]`` section. The 
value is in seconds. This will impact all users using ``airflowctl``.
 
diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index ec703ebf69c..7c03dae3055 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -53,7 +53,6 @@ from airflowctl.api.operations import (
 )
 from airflowctl.exceptions import (
     AirflowCtlCredentialNotFoundException,
-    AirflowCtlException,
     AirflowCtlKeyringException,
     AirflowCtlNotFoundException,
 )
@@ -161,8 +160,13 @@ class Credentials:
         """Generate path for the CLI config file."""
         return f"{self.api_environment}.json"
 
-    def save(self):
-        """Save the credentials to keyring and URL to disk as a file."""
+    def save(self, skip_keyring: bool = False):
+        """
+        Save the credentials to keyring and URL to disk as a file.
+
+        Skip saving the token to keyring if skip_keyring is True, in this case,
+        only the config file with the API URL is created.
+        """
         default_config_dir = os.environ.get("AIRFLOW_HOME", 
os.path.expanduser("~/airflow"))
         os.makedirs(default_config_dir, exist_ok=True)
         with open(os.path.join(default_config_dir, 
self.input_cli_config_file), "w") as f:
@@ -175,6 +179,8 @@ class Credentials:
                 ) as f:
                     json.dump({f"api_token_{self.api_environment}": 
self.api_token}, f)
             else:
+                if skip_keyring:
+                    return
                 # Replace the upstream EncryptedKeyring's unbounded password
                 # prompt with a bounded one before set_password can trigger it.
                 # The active backend may be a ChainerBackend that delegates to
@@ -184,11 +190,15 @@ class Credentials:
                 for candidate in candidates:
                     if hasattr(candidate, "_get_new_password"):
                         candidate._get_new_password = _bounded_get_new_password
-                keyring.set_password("airflowctl", 
f"api_token_{self.api_environment}", self.api_token)
-        except NoKeyringError as e:
+                keyring.set_password("airflowctl", 
f"api_token_{self.api_environment}", self.api_token)  # type: ignore[arg-type]
+        except (NoKeyringError, NotImplementedError) as e:
             log.error(e)
             raise AirflowCtlKeyringException(
-                "Keyring backend is not available. Cannot save credentials."
+                "Keyring backend is not available. Cannot save credentials.\n"
+                "The api url config was saved and you can still use airflowctl 
"
+                "by setting the AIRFLOW_CLI_TOKEN environment variable or 
passing "
+                "the --api-token flag to any command.\n"
+                "Use `airflowctl auth login --skip-keyring ...` to dismiss 
this error."
             ) from e
         except TypeError as e:
             # This happens when the token is None, which is not allowed by 
keyring
@@ -203,6 +213,8 @@ class Credentials:
             with open(config_path) as f:
                 credentials = json.load(f)
                 self.api_url = credentials["api_url"]
+                if self.api_token is not None:
+                    return self
                 if os.getenv("AIRFLOW_CLI_DEBUG_MODE") == "true":
                     debug_creds_path = os.path.join(
                         default_config_dir, 
f"debug_creds_{self.input_cli_config_file}"
@@ -232,13 +244,9 @@ class Credentials:
                             raise AirflowCtlKeyringException("Keyring backend 
is not available") from e
                         self.api_token = None
         except FileNotFoundError:
-            if self.client_kind == ClientKind.AUTH:
-                # Saving the URL set from the Auth Commands if Kind is AUTH
-                self.save()
-            elif self.client_kind == ClientKind.CLI:
+            # This is expected during the auth login command
+            if self.client_kind != ClientKind.AUTH:
                 raise AirflowCtlCredentialNotFoundException("No credentials 
file found. Please login first.")
-            else:
-                raise AirflowCtlException(f"Unknown client kind: 
{self.client_kind}")
 
         return self
 
@@ -371,20 +379,21 @@ class Client(httpx.Client):
 
 # API Client Decorator for CLI Actions
 @contextlib.contextmanager
-def get_client(kind: Literal[ClientKind.CLI, ClientKind.AUTH] = 
ClientKind.CLI):
+def get_client(kind: Literal[ClientKind.CLI, ClientKind.AUTH] = 
ClientKind.CLI, api_token: str | None = None):
     """
     Get CLI API client.
 
     Don't call this method, please use @provide_api_client decorator instead.
     """
     api_client = None
+    api_token = api_token or os.getenv("AIRFLOW_CLI_TOKEN", None)
     try:
         # API URL always loaded from the config file, please save with it if 
you are using other than ClientKind.CLI
-        credentials = Credentials(client_kind=kind).load()
+        credentials = Credentials(client_kind=kind, api_token=api_token).load()
         api_client = Client(
             base_url=credentials.api_url or "http://localhost:8080";,
             limits=httpx.Limits(max_keepalive_connections=1, 
max_connections=1),
-            token=credentials.api_token or str(os.getenv("AIRFLOW_CLI_TOKEN", 
"")),
+            token=str(api_token or credentials.api_token),
             kind=kind,
         )
         yield api_client
@@ -412,7 +421,8 @@ def provide_api_client(
         @wraps(func)
         def wrapper(*args, **kwargs) -> RT:
             if "api_client" not in kwargs:
-                with get_client(kind=kind) as api_client:
+                api_token = getattr(args[0], "api_token", None) if args else 
None
+                with get_client(kind=kind, api_token=api_token) as api_client:
                     return func(*args, api_client=api_client, **kwargs)
             # The CLI API Client should be only passed for Mocking and Testing
             return func(*args, **kwargs)
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
old mode 100644
new mode 100755
index 5f7114480c2..fcc0762b9f6
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -243,6 +243,13 @@ ARG_AUTH_USERNAME = Arg(
     dest="username",
     help="The username to use for authentication",
 )
+ARG_AUTH_SKIP_KEYRING = Arg(
+    flags=("--skip-keyring",),
+    dest="skip_keyring",
+    default=False,
+    action="store_true",
+    help="Skip storing credentials in keyring",
+)
 ARG_AUTH_PASSWORD = Arg(
     flags=("--password",),
     type=str,
@@ -749,6 +756,23 @@ class CommandFactory:
         return self.group_commands_list
 
 
+def add_auth_token_to_all_commands(commands: Iterable[CLICommand]) -> 
list[CLICommand]:
+    """Add ARG_AUTH_TOKEN to all ActionCommands."""
+    new_commands: list[CLICommand] = []
+    for command in commands:
+        if isinstance(command, ActionCommand):
+            new_args = list(command.args)
+            if ARG_AUTH_TOKEN not in new_args:
+                new_args.append(ARG_AUTH_TOKEN)
+            new_commands.append(command._replace(args=new_args))
+        elif isinstance(command, GroupCommand):
+            new_subcommands = 
add_auth_token_to_all_commands(command.subcommands)
+            new_commands.append(command._replace(subcommands=new_subcommands))
+        else:
+            new_commands.append(command)
+    return new_commands
+
+
 def merge_commands(
     base_commands: list[CLICommand], commands_will_be_merged: list[CLICommand]
 ) -> list[CLICommand]:
@@ -812,7 +836,13 @@ AUTH_COMMANDS = (
         help="Login to the metadata database for personal usage. JWT Token 
must be provided via parameter.",
         description="Login to the metadata database",
         func=lazy_load_command("airflowctl.ctl.commands.auth_command.login"),
-        args=(ARG_AUTH_URL, ARG_AUTH_TOKEN, ARG_AUTH_ENVIRONMENT, 
ARG_AUTH_USERNAME, ARG_AUTH_PASSWORD),
+        args=(
+            ARG_AUTH_URL,
+            ARG_AUTH_ENVIRONMENT,
+            ARG_AUTH_USERNAME,
+            ARG_AUTH_PASSWORD,
+            ARG_AUTH_SKIP_KEYRING,
+        ),
     ),
     ActionCommand(
         name="list-envs",
@@ -943,3 +973,5 @@ core_commands: list[CLICommand] = [
 core_commands = merge_commands(
     base_commands=command_factory.group_commands, 
commands_will_be_merged=core_commands
 )
+# Add ARG_AUTH_TOKEN to all commands
+core_commands = add_auth_token_to_all_commands(core_commands)
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
index 8cc55ff69e8..722219f52ba 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/auth_command.py
@@ -38,7 +38,9 @@ def login(args, api_client=NEW_API_CLIENT) -> None:
     success_message = "[green]Login successful! Welcome to airflowctl![/green]"
     # Check is username and password are passed
     if args.username and args.password:
-        # Generate empty credentials with the api_url and env
+        if args.skip_keyring:
+            rich.print("[red]The --skip-keyring is not compatible with 
username and password login.")
+            sys.exit(1)
         credentials = Credentials(
             api_url=args.api_url,
             api_token="",
@@ -47,7 +49,6 @@ def login(args, api_client=NEW_API_CLIENT) -> None:
         )
         # After logging in, the token will be saved in the credentials file
         try:
-            credentials.save()
             api_client.refresh_base_url(base_url=args.api_url, 
kind=ClientKind.AUTH)
             login_response = api_client.login.login_with_username_and_password(
                 LoginBody(
@@ -78,7 +79,7 @@ def login(args, api_client=NEW_API_CLIENT) -> None:
         api_url=args.api_url,
         api_token=token,
         api_environment=args.env,
-    ).save()
+    ).save(args.skip_keyring)
     rich.print(success_message)
 
 
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py 
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index 8d4a24c28b7..f79322d16fb 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -126,6 +126,38 @@ class TestCredentials:
                 "api_url": credentials.api_url,
             }
 
+    @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": 
"TEST_SAVE_NO_KEYRING"})
+    @patch("airflowctl.api.client.keyring")
+    def test_save_no_keyring(self, mock_keyring):
+        from keyring.errors import NoKeyringError
+
+        cli_client = ClientKind.CLI
+        mock_keyring.set_password.side_effect = NoKeyringError("no backend")
+
+        with pytest.raises(AirflowCtlKeyringException, match="Keyring backend 
is not available"):
+            Credentials(client_kind=cli_client).save()
+
+    @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": 
"TEST_SAVE_SKIP_KEYRING"})
+    @patch("airflowctl.api.client.keyring")
+    def test_save_no_keyring_backend_skip_keyring(self, mock_keyring):
+
+        env = "TEST_SAVE_SKIP_KEYRING"
+        cli_client = ClientKind.CLI
+        mock_keyring.set_password = MagicMock()
+        mock_keyring.get_password = MagicMock()
+
+        Credentials(client_kind=cli_client).save(skip_keyring=True)
+
+        config_dir = os.environ.get("AIRFLOW_HOME", 
os.path.expanduser("~/airflow"))
+        assert os.path.exists(config_dir)
+        with open(os.path.join(config_dir, f"{env}.json")) as f:
+            credentials = Credentials(client_kind=cli_client, 
api_token="TEST_TOKEN").load()
+            assert json.load(f) == {
+                "api_url": credentials.api_url,
+            }
+        mock_keyring.set_password.assert_not_called()
+        mock_keyring.get_password.assert_not_called()
+
     @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_LOAD"})
     @patch.dict(os.environ, {"AIRFLOW_CLI_TOKEN": "TEST_TOKEN"})
     @patch("airflowctl.api.client.keyring")
@@ -196,6 +228,21 @@ class TestCredentials:
         with pytest.raises(AirflowCtlKeyringException, match="Keyring backend 
is not available"):
             Credentials(client_kind=cli_client).load()
 
+    @patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": 
"TEST_NO_KEYRING_BACKEND"})
+    @patch("airflowctl.api.client.keyring")
+    def test_load_no_keyring_backend_token_provided(self, mock_keyring):
+        from keyring.errors import NoKeyringError
+
+        cli_client = ClientKind.CLI
+        config_dir = os.environ.get("AIRFLOW_HOME", 
os.path.expanduser("~/airflow"))
+        os.makedirs(config_dir, exist_ok=True)
+        with open(os.path.join(config_dir, "TEST_NO_KEYRING_BACKEND.json"), 
"w") as f:
+            json.dump({"api_url": "http://localhost:8080"}, f)
+        mock_keyring.get_password.side_effect = NoKeyringError("no backend")
+
+        credentials = Credentials(client_kind=cli_client, 
api_token="TEST_TOKEN").load()
+        assert credentials.api_token == "TEST_TOKEN"
+
 
 class TestBoundedGetNewPassword:
     @patch("airflowctl.api.client.getpass.getpass")
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
index 54fb83901ee..70d19d26657 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
@@ -70,6 +70,52 @@ class TestCliAuthCommands:
                 "airflowctl", "api_token_TEST_AUTH_LOGIN", "TEST_TOKEN"
             )
 
+    @patch.dict(os.environ, {"AIRFLOW_CLI_TOKEN": "TEST_TOKEN"})
+    @patch("airflowctl.api.client.keyring")
+    def test_login_with_skip_keyring(self, mock_keyring, api_client_maker):
+        from keyring.errors import NoKeyringError
+
+        api_client = api_client_maker(
+            path="/auth/token/cli",
+            response_json=self.login_response.model_dump(),
+            expected_http_status_code=201,
+            kind=ClientKind.AUTH,
+        )
+
+        mock_keyring.set_password.side_effect = NoKeyringError("no backend")
+        with (
+            patch("sys.stdin", io.StringIO("test_password")),
+            patch("airflowctl.ctl.cli_config.getpass.getpass", 
return_value="test_password"),
+        ):
+            auth_command.login(
+                self.parser.parse_args(
+                    ["auth", "login", "--skip-keyring", "--api-url", 
"http://localhost:8080";]
+                ),
+                api_client=api_client,
+            )
+
+    @patch("airflowctl.api.client.keyring")
+    def test_login_without_skip_keyring_raises_on_no_keyring(self, 
mock_keyring, api_client_maker):
+        from keyring.errors import NoKeyringError
+
+        api_client = api_client_maker(
+            path="/auth/token/cli",
+            response_json=self.login_response.model_dump(),
+            expected_http_status_code=201,
+            kind=ClientKind.AUTH,
+        )
+
+        mock_keyring.set_password.side_effect = NoKeyringError("no backend")
+        with (
+            patch("sys.stdin", io.StringIO("test_password")),
+            patch("airflowctl.ctl.cli_config.getpass.getpass", 
return_value="test_password"),
+            pytest.raises(SystemExit, match="1"),
+        ):
+            auth_command.login(
+                self.parser.parse_args(["auth", "login", "--api-url", 
"http://localhost:8080";]),
+                api_client=api_client,
+            )
+
     # Test auth login with username and password
     @patch("airflowctl.api.client.keyring")
     def test_login_with_username_and_password(self, mock_keyring, 
api_client_maker):
@@ -102,7 +148,6 @@ class TestCliAuthCommands:
             )
             mock_keyring.set_password.assert_has_calls(
                 [
-                    mock.call("airflowctl", "api_token_production", ""),
                     mock.call("airflowctl", "api_token_production", 
"TEST_TOKEN"),
                 ]
             )
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
index 44cc341ee1c..5ebfdc869c8 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_pool_command.py
@@ -48,21 +48,21 @@ class TestPoolImportCommand:
         """Test import with missing file."""
         non_existent = tmp_path / "non_existent.json"
         with pytest.raises(SystemExit, match=f"Missing pools file 
{non_existent}"):
-            pool_command.import_(args=mock.MagicMock(file=non_existent))
+            pool_command.import_(mock.MagicMock(file=non_existent))
 
     def test_import_invalid_json(self, mock_client, tmp_path):
         """Test import with invalid JSON file."""
         invalid_json = tmp_path / "invalid.json"
         invalid_json.write_text("invalid json")
         with pytest.raises(SystemExit, match="Invalid json file"):
-            pool_command.import_(args=mock.MagicMock(file=invalid_json))
+            pool_command.import_(mock.MagicMock(file=invalid_json))
 
     def test_import_invalid_pool_config(self, mock_client, tmp_path):
         """Test import with invalid pool configuration."""
         invalid_pool = tmp_path / "invalid_pool.json"
         invalid_pool.write_text(json.dumps([{"invalid": "config"}]))
         with pytest.raises(SystemExit, match="Invalid pool configuration: 
{'invalid': 'config'}"):
-            pool_command.import_(args=mock.MagicMock(file=invalid_pool))
+            pool_command.import_(mock.MagicMock(file=invalid_pool))
 
     def test_import_success(self, mock_client, tmp_path, capsys):
         """Test successful pool import."""
@@ -87,7 +87,7 @@ class TestPoolImportCommand:
 
         mock_client.pools.bulk.return_value = mock_bulk_builder
 
-        pool_command.import_(args=mock.MagicMock(file=pools_file))
+        pool_command.import_(mock.MagicMock(file=pools_file))
 
         # Verify bulk operation was called with correct parameters
         mock_client.pools.bulk.assert_called_once()
@@ -134,7 +134,7 @@ class TestPoolExportCommand:
         mock_pools.total_entries = 1
         mock_client.pools.list.return_value = mock_pools
 
-        pool_command.export(args=mock.MagicMock(file=export_file, 
output="json"))
+        pool_command.export(mock.MagicMock(file=export_file, output="json"))
 
         # Verify the exported file content
         exported_data = json.loads(export_file.read_text())
@@ -170,7 +170,7 @@ class TestPoolExportCommand:
         mock_pools.total_entries = 1
         mock_client.pools.list.return_value = mock_pools
 
-        pool_command.export(args=mock.MagicMock(file=tmp_path / "unused.json", 
output="table"))
+        pool_command.export(mock.MagicMock(file=tmp_path / "unused.json", 
output="table"))
 
         # Verify console output contains the raw dict
         captured = capsys.readouterr()
@@ -185,4 +185,4 @@ class TestPoolExportCommand:
         mock_client.pools.list.side_effect = Exception("API Error")
 
         with pytest.raises(SystemExit, match="Failed to export pools: API 
Error"):
-            pool_command.export(args=mock.MagicMock(file=export_file, 
output="json"))
+            pool_command.export(mock.MagicMock(file=export_file, 
output="json"))
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 3173aa4d353..6d5bf39e7ab 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -22,7 +22,15 @@ from textwrap import dedent
 
 import pytest
 
-from airflowctl.ctl.cli_config import ActionCommand, CommandFactory, 
GroupCommand, merge_commands
+from airflowctl.ctl.cli_config import (
+    ARG_AUTH_TOKEN,
+    ActionCommand,
+    Arg,
+    CommandFactory,
+    GroupCommand,
+    add_auth_token_to_all_commands,
+    merge_commands,
+)
 
 
 @pytest.fixture
@@ -356,6 +364,68 @@ class TestCliConfigMethods:
                 assert "subcommand3" in sub_command_names
                 assert "subcommand4" in sub_command_names
 
+    def test_add_auth_token_to_all_commands(self, no_op_method):
+        """Test the add_auth_token_to_all_commands method."""
+        ARG_1 = Arg(
+            flags=("--arg1",),
+        )
+        ARG_2 = Arg(
+            flags=("--arg1",),
+        )
+        action_commands_1 = (
+            ActionCommand(
+                name="subcommand1",
+                help="This is subcommand 1",
+                func=no_op_method,
+                args=(),
+            ),
+            ActionCommand(
+                name="subcommand2",
+                help="This is subcommand 2",
+                func=no_op_method,
+                args=(ARG_1,),
+            ),
+        )
+        command_list = [
+            GroupCommand(
+                name="command1",
+                help="This is command 1 new help",
+                description="This is command 1 new description",
+                subcommands=action_commands_1,
+            ),
+            ActionCommand(
+                name="command2",
+                help="This is command 2",
+                func=no_op_method,
+                args=(ARG_1, ARG_2),
+            ),
+        ]
+
+        command_list = add_auth_token_to_all_commands(command_list)
+
+        merged_command_names = [command.name for command in command_list]
+        assert "command1" in merged_command_names
+        assert "command2" in merged_command_names
+        assert len(command_list) == 2
+
+        expected_subcommand_1_args = [ARG_AUTH_TOKEN]
+        expected_subcommand_2_args = [ARG_1, ARG_AUTH_TOKEN]
+        expected_command_2_args = [ARG_1, ARG_2, ARG_AUTH_TOKEN]
+
+        for command in command_list:
+            if command.name == "command1":
+                sub_command_names = [sc.name for sc in 
list(command.subcommands)]
+                assert "subcommand1" in sub_command_names
+                assert "subcommand2" in sub_command_names
+                assert len(sub_command_names) == 2
+                for sub_command in command.subcommands:
+                    if sub_command.name == "subcommand1":
+                        assert sub_command.args == expected_subcommand_1_args
+                    if sub_command.name == "subcommand2":
+                        assert sub_command.args == expected_subcommand_2_args
+            if command.name == "command2":
+                assert command.args == expected_command_2_args
+
     def test_trigger_dag_run_defaults_logical_date_to_now(self):
         """Test that trigger command defaults logical_date to now when not 
provided."""
         from datetime import datetime, timezone

Reply via email to