Skip to content
Merged
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
110 changes: 0 additions & 110 deletions .github/workflows/cleanup-endpoints.yml

This file was deleted.

24 changes: 17 additions & 7 deletions runpod/cli/groups/config/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ def set_credentials(api_key: str, profile: str = "default", overwrite=False) ->
Path(CREDENTIAL_FILE).touch(exist_ok=True)

if not overwrite:
with open(CREDENTIAL_FILE, "rb") as cred_file:
if profile in toml.load(cred_file):
raise ValueError(
"Profile already exists. Use `update_credentials` instead."
)
try:
with open(CREDENTIAL_FILE, "rb") as cred_file:
existing = toml.load(cred_file)
except (TypeError, ValueError):
existing = {}
if profile in existing:
raise ValueError(
"Profile already exists. Use `update_credentials` instead."
)

with open(CREDENTIAL_FILE, "w", encoding="UTF-8") as cred_file:
cred_file.write("[" + profile + "]\n")
Expand Down Expand Up @@ -72,12 +76,18 @@ def check_credentials(profile: str = "default"):
def get_credentials(profile="default"):
"""
Returns the credentials for the specified profile from ~/.runpod/config.toml

Returns None if the file does not exist, is not valid TOML, or does not
contain the requested profile.
"""
if not os.path.exists(CREDENTIAL_FILE):
return None

with open(CREDENTIAL_FILE, "rb") as cred_file:
credentials = toml.load(cred_file)
try:
with open(CREDENTIAL_FILE, "rb") as cred_file:
credentials = toml.load(cred_file)
except (TypeError, ValueError):
return None

if profile not in credentials:
return None
Expand Down
33 changes: 14 additions & 19 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,17 @@ def endpoints(require_api_key, test_cases):
log.info("Endpoint ready: name=%s image=%s template.dockerArgs=%s", ep.name, ep.image, ep.template.dockerArgs if ep.template else "N/A")
yield eps

# Undeploy only the endpoints provisioned by this test run.
# Uses by-name undeploy to avoid tearing down unrelated endpoints
# sharing the same API key (parallel CI runs, developer endpoints).
endpoint_names = [ep.name for ep in eps.values()]
log.info("Cleaning up %d provisioned endpoints: %s", len(endpoint_names), endpoint_names)
for name in endpoint_names:
try:
result = subprocess.run(
["flash", "undeploy", name, "--force"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode == 0:
log.info("Undeployed %s", name)
else:
log.warning("flash undeploy %s failed (rc=%d): %s", name, result.returncode, result.stderr)
except Exception:
log.exception("Failed to undeploy %s", name)
log.info("Cleaning up all provisioned endpoints")
try:
result = subprocess.run(
["flash", "undeploy", "--all", "--force"],
capture_output=True,
text=True,
timeout=120,
)
if result.returncode == 0:
log.info("Undeployed all endpoints")
else:
log.warning("flash undeploy --all --force failed (rc=%d): %s", result.returncode, result.stderr)
except Exception:
log.exception("Failed to undeploy endpoints")
50 changes: 50 additions & 0 deletions tests/test_cli/test_cli_groups/test_config_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,53 @@ def test_get_credentials_non_existent_profile(
assert result is None
assert mock_open_call.called
assert mock_exists.called

@patch("runpod.cli.groups.config.functions.os.path.exists", return_value=True)
@patch(
"runpod.cli.groups.config.functions.toml.load",
side_effect=ValueError("Invalid value"),
)
@patch("builtins.open", new_callable=mock_open)
def test_get_credentials_corrupted_toml(
self, _mock_open_call, _mock_toml_load, _mock_exists
):
"""get_credentials returns None when config.toml contains invalid TOML."""
result = functions.get_credentials("default")
assert result is None

@patch("runpod.cli.groups.config.functions.os.path.exists", return_value=True)
@patch(
"runpod.cli.groups.config.functions.toml.load",
side_effect=TypeError("bad type"),
)
@patch("builtins.open", new_callable=mock_open)
def test_get_credentials_type_error(
self, _mock_open_call, _mock_toml_load, _mock_exists
):
"""get_credentials returns None on TypeError from corrupted file."""
result = functions.get_credentials("default")
assert result is None

@patch("runpod.cli.groups.config.functions.Path.touch")
@patch("runpod.cli.groups.config.functions.os.makedirs")
@patch("runpod.cli.groups.config.functions.toml.load")
@patch("builtins.open", new_callable=mock_open())
def test_set_credentials_corrupted_toml_allows_overwrite(
self, _mock_file, mock_toml_load, _mock_makedirs, _mock_touch
):
"""set_credentials with overwrite=True ignores corrupted existing file."""
mock_toml_load.side_effect = ValueError("Invalid TOML")
# overwrite=True skips the toml.load check entirely
functions.set_credentials("NEW_KEY", overwrite=True)

@patch("runpod.cli.groups.config.functions.Path.touch")
@patch("runpod.cli.groups.config.functions.os.makedirs")
@patch("runpod.cli.groups.config.functions.toml.load")
@patch("builtins.open", new_callable=mock_open())
def test_set_credentials_corrupted_toml_no_overwrite(
self, _mock_file, mock_toml_load, _mock_makedirs, _mock_touch
):
"""set_credentials without overwrite treats corrupted file as empty."""
mock_toml_load.side_effect = ValueError("Invalid TOML")
# Should not raise — corrupted file is treated as having no profiles
functions.set_credentials("NEW_KEY", overwrite=False)
Loading