Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions airflow-ctl/src/airflowctl/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,9 @@ def load(self) -> Credentials:
raise AirflowCtlKeyringException("Keyring backend is not available") from e
self.api_token = None
except FileNotFoundError:
# This is expected during the auth login command
if self.client_kind != ClientKind.AUTH:
# This is expected during the auth login command.
# Also allow token-only usage without local config (for commands like `version --remote`).
if self.client_kind != ClientKind.AUTH and self.api_token is None:
Comment thread
rjgoyln marked this conversation as resolved.
Outdated
raise AirflowCtlCredentialNotFoundException("No credentials file found. Please login first.")

return self
Expand Down
18 changes: 17 additions & 1 deletion airflow-ctl/tests/airflow_ctl/api/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import time_machine
from httpx import URL

from airflowctl.api.client import Client, ClientKind, Credentials, _bounded_get_new_password
from airflowctl.api.client import Client, ClientKind, Credentials, _bounded_get_new_password, get_client
from airflowctl.api.operations import ServerResponseError
from airflowctl.exceptions import (
AirflowCtlCredentialNotFoundException,
Expand Down Expand Up @@ -214,6 +214,16 @@ def test_load_no_credentials(self, mock_keyring):

assert not os.path.exists(config_dir)

@patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_NO_CONFIG_WITH_EXPLICIT_TOKEN"})
@patch("airflowctl.api.client.keyring")
def test_load_no_config_with_explicit_token(self, mock_keyring):
cli_client = ClientKind.CLI
credentials = Credentials(client_kind=cli_client, api_token="TEST_TOKEN").load()

assert credentials.api_token == "TEST_TOKEN"
assert credentials.api_url is None
mock_keyring.get_password.assert_not_called()

@patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_KEYRING_VALUE_ERROR"})
@patch("airflowctl.api.client.keyring")
def test_load_incorrect_keyring_password(self, mock_keyring):
Expand Down Expand Up @@ -400,6 +410,12 @@ def test_credentials_accepts_safe_env():
assert creds.api_environment == "prod-us_1"


@patch.dict(os.environ, {"AIRFLOW_CLI_ENVIRONMENT": "TEST_GET_CLIENT_WITH_TOKEN_ONLY"})
def test_get_client_allows_explicit_token_without_config():
with get_client(kind=ClientKind.CLI, api_token="TEST_TOKEN") as client:
assert str(client.base_url) == "http://localhost:8080/api/v2/"


@pytest.mark.parametrize("api_environment", ["../evil", "..\\evil", "a/b", "a\\b"])
def test_credentials_rejects_unsafe_env_argument(api_environment):
with pytest.raises(AirflowCtlException, match="environment"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import pytest

from airflowctl.api.client import Client
from airflowctl.api.client import Client, ClientKind
from airflowctl.ctl import cli_parser
from airflowctl.ctl.commands.version_command import version_info

Expand Down Expand Up @@ -53,6 +53,12 @@ def test_ctl_version_remote(self, mock_client):
assert "git_version" in stdout.getvalue()
assert "airflowctl_version" in stdout.getvalue()

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.CLI, api_token="TOKEN")

def test_ctl_version_only_local_version(self, mock_client):
"""Test the version command without --remote does not touch credentials."""
with redirect_stdout(StringIO()) as stdout:
Expand Down
Loading