From e735cef34ae555f10f827bdf77debecd2d5ab2ab Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 25 Aug 2026 10:39:05 -0400 Subject: [PATCH] fix(credentials): read runpodctl top-level apikey as fallback (#363) get_api_key() only consulted runpod-python's [default] profile, so a config.toml written by runpodctl (top-level apikey/apiurl, no profile table) was read without error yet yielded no key, making flash report 'No RunPod API key found' despite a valid key sitting in the file. Add a file-based fallback: when the profile lookup finds no usable api_key, parse ~/.runpod/config.toml directly (tomllib) and return the top-level apikey. Precedence is unchanged: env var > [default].api_key > runpodctl top-level apikey. apiurl stays ignored. Fixes runpod/flash#363 --- src/runpod_flash/core/credentials.py | 31 +++++++++++++++++++++++--- tests/unit/test_credentials.py | 33 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/runpod_flash/core/credentials.py b/src/runpod_flash/core/credentials.py index a984bf32..0abc1888 100644 --- a/src/runpod_flash/core/credentials.py +++ b/src/runpod_flash/core/credentials.py @@ -51,10 +51,35 @@ def get_api_key() -> Optional[str]: creds = get_credentials() except Exception: log.debug("Failed to read credentials file", exc_info=True) - return None - if creds and isinstance(creds.get("api_key"), str) and creds["api_key"].strip(): - return creds["api_key"] + else: + if creds and isinstance(creds.get("api_key"), str) and creds["api_key"].strip(): + return creds["api_key"] + + return _get_runpodctl_api_key() + +def _get_runpodctl_api_key() -> Optional[str]: + """Read runpodctl's top-level `apikey` from the config file. + + runpodctl writes a top-level `apikey` (and `apiurl`) key with no profile + table, which runpod-python's profile-based lookup cannot see. Fall back to + parsing the file directly so a config written by runpodctl authenticates + flash. Returns None when the file or key is missing, malformed, or blank. + """ + try: + import tomllib + except ImportError: + import tomli as tomllib + + try: + with get_credentials_path().open("rb") as f: + data = tomllib.load(f) + except (OSError, ValueError): + log.debug("Failed to read credentials file for runpodctl apikey", exc_info=True) + return None + api_key = data.get("apikey") + if isinstance(api_key, str) and api_key.strip(): + return api_key return None diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py index 5ef368a2..066a530c 100644 --- a/tests/unit/test_credentials.py +++ b/tests/unit/test_credentials.py @@ -11,6 +11,8 @@ else: import tomli as tomllib +import pytest + from runpod_flash.core.credentials import ( get_api_key, get_credentials_path, @@ -57,6 +59,37 @@ def test_handles_corrupt_credentials_file(self, isolate_credentials_file): isolate_credentials_file.write_text("not valid toml {{{{") assert get_api_key() is None + def test_falls_back_to_runpodctl_top_level_apikey(self, isolate_credentials_file): + """A config.toml written by runpodctl (top-level `apikey`, no [default] + profile) must authenticate flash.""" + isolate_credentials_file.parent.mkdir(parents=True, exist_ok=True) + isolate_credentials_file.write_text( + "apikey = 'rpa_runpodctl_key'\napiurl = 'https://api.runpod.io/graphql'\n" + ) + assert get_api_key() == "rpa_runpodctl_key" + + def test_default_profile_takes_precedence_over_runpodctl_apikey( + self, isolate_credentials_file + ): + isolate_credentials_file.parent.mkdir(parents=True, exist_ok=True) + isolate_credentials_file.write_text( + "apikey = 'rpa_runpodctl_key'\n[default]\napi_key = 'flash-key'\n" + ) + assert get_api_key() == "flash-key" + + def test_ignores_blank_runpodctl_apikey(self, isolate_credentials_file): + isolate_credentials_file.parent.mkdir(parents=True, exist_ok=True) + isolate_credentials_file.write_text("apikey = ' '\n") + assert get_api_key() is None + + def test_no_file_still_raises_runpod_api_key_error(self, isolate_credentials_file): + """With no credentials file at all, validation must raise.""" + from runpod_flash.core.exceptions import RunpodAPIKeyError + from runpod_flash.core.validation import validate_api_key + + with pytest.raises(RunpodAPIKeyError): + validate_api_key() + class TestSaveApiKey: def test_creates_file_and_directories(self, isolate_credentials_file):