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

shahar1 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 aee26d49b6b Fix airflowctl ignoring --env for non-auth commands 
(#73808)
aee26d49b6b is described below

commit aee26d49b6bdf9713e46e012cd3d7516f2cc7d1e
Author: kuse <[email protected]>
AuthorDate: Tue Oct 27 18:48:25 2026 +0800

    Fix airflowctl ignoring --env for non-auth commands (#73808)
    
    Honor --env when resolving airflowctl API credentials
    
    Forward the selected environment through decorated commands and version 
--remote, so staging requests use staging credentials.
---
 airflow-ctl/src/airflowctl/api/client.py           | 15 ++++++--
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  2 -
 .../src/airflowctl/ctl/commands/version_command.py |  3 +-
 airflow-ctl/tests/airflow_ctl/api/test_client.py   | 44 ++++++++++++++++++++++
 .../ctl/commands/test_version_command.py           | 10 ++++-
 5 files changed, 66 insertions(+), 8 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index b1772747346..5ca17140357 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -516,6 +516,7 @@ class Client(httpx.Client):
 def get_client(
     kind: Literal[ClientKind.CLI, ClientKind.AUTH, ClientKind.NO_AUTH] = 
ClientKind.CLI,
     api_token: str | None = None,
+    api_environment: str = "production",
 ):
     """
     Get CLI API client.
@@ -527,10 +528,12 @@ def get_client(
     try:
         # API URL always loaded from the config file, please save with it if 
you are using other than ClientKind.CLI
         if kind == ClientKind.NO_AUTH:
-            credentials = Credentials(client_kind=kind).load()
+            credentials = Credentials(client_kind=kind, 
api_environment=api_environment).load()
             resolved_token = None
         else:
-            credentials = Credentials(client_kind=kind, 
api_token=api_token).load()
+            credentials = Credentials(
+                client_kind=kind, api_token=api_token, 
api_environment=api_environment
+            ).load()
             resolved_token = api_token or credentials.api_token
         api_client = Client(
             base_url=credentials.api_url or "http://localhost:8080";,
@@ -562,7 +565,13 @@ def provide_api_client(
         def wrapper(*args, **kwargs) -> RT:
             if "api_client" not in kwargs:
                 api_token = getattr(args[0], "api_token", None) if args else 
None
-                with get_client(kind=kind, api_token=api_token) as api_client:
+                # ``env`` comes from ARG_AUTH_ENVIRONMENT on every command that
+                # accepts ``--env``/``-e``; Credentials itself still honours
+                # AIRFLOW_CLI_ENVIRONMENT over this value.
+                api_environment = (getattr(args[0], "env", None) if args else 
None) or "production"
+                with get_client(
+                    kind=kind, api_token=api_token, 
api_environment=api_environment
+                ) 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
index b0f229be2ff..25c14203b67 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -837,8 +837,6 @@ class CommandFactory:
                         )
 
             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)
 
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/version_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/version_command.py
index 42be64980ef..ccddbd2079c 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/version_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/version_command.py
@@ -26,7 +26,8 @@ def version_info(arg):
     """Get version information."""
     version_dict = {"airflowctl_version": airflowctl_version}
     if arg.remote:
-        with get_client(kind=ClientKind.NO_AUTH) as api_client:
+        api_environment = getattr(arg, "env", None) or "production"
+        with get_client(kind=ClientKind.NO_AUTH, 
api_environment=api_environment) as api_client:
             version_response = api_client.version.get()
             version_dict.update(version_response.model_dump())
     rich.print(version_dict)
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py 
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index df104786dab..049c384b652 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -23,8 +23,10 @@ import shutil
 import subprocess
 import sys
 import tempfile
+from types import SimpleNamespace
 from typing import Any
 from unittest.mock import MagicMock, patch
+from urllib.parse import urlparse
 
 import httpx
 import pytest
@@ -40,6 +42,7 @@ from airflowctl.api.client import (
     _bounded_get_new_password,
     get_client,
     get_json_error,
+    provide_api_client,
 )
 from airflowctl.api.operations import ServerResponseError
 from airflowctl.exceptions import (
@@ -710,3 +713,44 @@ class TestRetryConfigurationEnvVars:
         assert result.returncode == 0, result.stderr
         assert result.stdout.strip() == "0"
         assert result.stderr == ""
+
+
+class TestGetClientEnvironment:
+    """``get_client`` and ``@provide_api_client`` commands resolve the config 
file for the requested environment."""
+
+    @staticmethod
+    def _write_environment_config(airflow_home, environment: str, api_url: 
str) -> None:
+        (airflow_home / 
f"{environment}.json").write_text(json.dumps({"api_url": api_url}), 
encoding="utf-8")
+
+    @pytest.fixture(autouse=True)
+    def environments(self, tmp_path):
+        """The module fixture already clears the environment; pin AIRFLOW_HOME 
to tmp_path."""
+        os.environ["AIRFLOW_HOME"] = str(tmp_path)
+        self._write_environment_config(tmp_path, "production", 
"https://prod.example.com";)
+        self._write_environment_config(tmp_path, "staging", 
"https://staging.example.com";)
+        yield tmp_path
+        del os.environ["AIRFLOW_HOME"]
+
+    def test_uses_the_requested_environment(self):
+        with get_client(kind=ClientKind.CLI, api_token="TOKEN", 
api_environment="staging") as client:
+            assert urlparse(str(client.base_url)).hostname == 
"staging.example.com"
+
+    def test_no_auth_uses_the_requested_environment(self):
+        with get_client(kind=ClientKind.NO_AUTH, api_environment="staging") as 
client:
+            assert urlparse(str(client.base_url)).hostname == 
"staging.example.com"
+
+    def test_environment_variable_beats_the_explicit_argument(self, 
monkeypatch):
+        """AIRFLOW_CLI_ENVIRONMENT keeps precedence, matching Credentials' own 
contract."""
+        monkeypatch.setenv("AIRFLOW_CLI_ENVIRONMENT", "staging")
+        with get_client(kind=ClientKind.CLI, api_token="TOKEN", 
api_environment="production") as client:
+            assert urlparse(str(client.base_url)).hostname == 
"staging.example.com"
+
+    def test_decorator_forwards_the_env_argument(self):
+        @provide_api_client(kind=ClientKind.CLI)
+        def command_under_test(args, api_client=None):
+            # Inspect the client inside the command: the wrapper closes it
+            # before returning.
+            return urlparse(str(api_client.base_url)).hostname == 
"staging.example.com"
+
+        args = SimpleNamespace(env="staging", api_token="TOKEN")
+        assert command_under_test(args) is True
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py
index 6168e874d60..4186a2fce46 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_version_command.py
@@ -52,13 +52,19 @@ class TestVersionCommand:
             assert "version" in stdout.getvalue()
             assert "git_version" in stdout.getvalue()
             assert "airflowctl_version" in stdout.getvalue()
-            mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH)
+            mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH, 
api_environment="production")
 
     def test_ctl_version_remote_with_api_token(self, mock_client):
         with mock.patch("airflowctl.ctl.commands.version_command.get_client") 
as mock_get_client:
             mock_get_client.return_value.__enter__.return_value = mock_client
             version_info(self.parser.parse_args(["version", "--remote", 
"--api-token", "TOKEN"]))
-            mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH)
+            mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH, 
api_environment="production")
+
+    def test_ctl_version_remote_with_env(self, mock_client):
+        with mock.patch("airflowctl.ctl.commands.version_command.get_client") 
as mock_get_client:
+            mock_get_client.return_value.__enter__.return_value = mock_client
+            version_info(self.parser.parse_args(["version", "--remote", 
"--env", "staging"]))
+            mock_get_client.assert_called_once_with(kind=ClientKind.NO_AUTH, 
api_environment="staging")
 
     def test_ctl_version_only_local_version(self, mock_client):
         """Test the version command without --remote does not touch 
credentials."""

Reply via email to