Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
31 changes: 28 additions & 3 deletions src/runpod_flash/core/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
else:
import tomli as tomllib

import pytest

from runpod_flash.core.credentials import (
get_api_key,
get_credentials_path,
Expand Down Expand Up @@ -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):
Expand Down
Loading