-
Notifications
You must be signed in to change notification settings - Fork 8
feat: Centralize RunPod HTTP authentication for all client types #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
deanq
merged 2 commits into
deanq/ae-1102-load-balancer-sls-resource
from
deanquinanola/ae-1196-consistent-http-auth
Jan 5, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import requests | ||
| from typing import Dict, List, Optional, Any | ||
| from pydantic import BaseModel, model_validator | ||
| from tetra_rp.core.utils.http import get_authenticated_requests_session | ||
| from .base import BaseResource | ||
|
|
||
|
|
||
|
|
@@ -38,7 +38,7 @@ def sync_input_fields(self): | |
|
|
||
|
|
||
| def update_system_dependencies( | ||
| template_id, token, system_dependencies, base_entry_cmd=None | ||
| template_id, system_dependencies, base_entry_cmd=None, token=None | ||
| ): | ||
| """ | ||
| Updates Runpod template with system dependencies installed via apt-get, | ||
|
|
@@ -83,12 +83,16 @@ def update_system_dependencies( | |
| "volumeMountPath": "/workspace", | ||
| } | ||
|
|
||
| headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} | ||
|
|
||
| url = f"https://rest.runpod.io/v1/templates/{template_id}/update" | ||
| response = requests.post(url, json=payload, headers=headers) | ||
|
|
||
| # Use centralized auth utility instead of manual header setup | ||
| # Note: token parameter is deprecated; uses RUNPOD_API_KEY environment variable | ||
|
||
| session = get_authenticated_requests_session() | ||
| try: | ||
| response = session.post(url, json=payload) | ||
| response.raise_for_status() | ||
| return response.json() | ||
| except Exception: | ||
| return {"error": "Invalid JSON response", "text": response.text} | ||
| except Exception as e: | ||
| return {"error": "Failed to update template", "details": str(e)} | ||
| finally: | ||
| session.close() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| """HTTP utilities for RunPod API communication.""" | ||
|
|
||
| import os | ||
| from typing import Optional | ||
|
|
||
| import httpx | ||
| import requests | ||
|
|
||
|
|
||
| def get_authenticated_httpx_client( | ||
| timeout: Optional[float] = None, | ||
| ) -> httpx.AsyncClient: | ||
| """Create httpx AsyncClient with RunPod authentication. | ||
|
|
||
| Automatically includes Authorization header if RUNPOD_API_KEY is set. | ||
| This provides a centralized place to manage authentication headers for | ||
| all RunPod HTTP requests, avoiding repetitive manual header addition. | ||
|
|
||
| Args: | ||
| timeout: Request timeout in seconds. Defaults to 30.0. | ||
|
|
||
| Returns: | ||
| Configured httpx.AsyncClient with Authorization header | ||
|
|
||
| Example: | ||
| async with get_authenticated_httpx_client() as client: | ||
| response = await client.post(url, json=data) | ||
|
|
||
| # With custom timeout | ||
| async with get_authenticated_httpx_client(timeout=60.0) as client: | ||
| response = await client.get(url) | ||
| """ | ||
| headers = {} | ||
| api_key = os.environ.get("RUNPOD_API_KEY") | ||
| if api_key: | ||
| headers["Authorization"] = f"Bearer {api_key}" | ||
|
|
||
| timeout_config = timeout if timeout is not None else 30.0 | ||
| return httpx.AsyncClient(timeout=timeout_config, headers=headers) | ||
|
|
||
|
|
||
| def get_authenticated_requests_session() -> requests.Session: | ||
| """Create requests Session with RunPod authentication. | ||
|
|
||
| Automatically includes Authorization header if RUNPOD_API_KEY is set. | ||
| Provides a centralized place to manage authentication headers for | ||
| synchronous RunPod HTTP requests. | ||
|
|
||
| Returns: | ||
| Configured requests.Session with Authorization header | ||
|
|
||
| Example: | ||
| session = get_authenticated_requests_session() | ||
| response = session.post(url, json=data, timeout=30.0) | ||
| # Remember to close: session.close() | ||
|
|
||
| # Or use as context manager | ||
| import contextlib | ||
| with contextlib.closing(get_authenticated_requests_session()) as session: | ||
| response = session.post(url, json=data) | ||
| """ | ||
| session = requests.Session() | ||
| api_key = os.environ.get("RUNPOD_API_KEY") | ||
| if api_key: | ||
| session.headers["Authorization"] = f"Bearer {api_key}" | ||
|
|
||
| return session |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| """Tests for HTTP utilities for RunPod API communication.""" | ||
|
|
||
| import requests | ||
| from tetra_rp.core.utils.http import ( | ||
| get_authenticated_httpx_client, | ||
| get_authenticated_requests_session, | ||
| ) | ||
|
|
||
|
|
||
| class TestGetAuthenticatedHttpxClient: | ||
| """Test the get_authenticated_httpx_client utility function.""" | ||
|
|
||
| def test_get_authenticated_httpx_client_with_api_key(self, monkeypatch): | ||
| """Test client includes auth header when API key is set.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-api-key-123") | ||
|
|
||
| client = get_authenticated_httpx_client() | ||
|
|
||
| assert client is not None | ||
| assert "Authorization" in client.headers | ||
| assert client.headers["Authorization"] == "Bearer test-api-key-123" | ||
|
|
||
| def test_get_authenticated_httpx_client_without_api_key(self, monkeypatch): | ||
| """Test client works without API key (no auth header).""" | ||
| monkeypatch.delenv("RUNPOD_API_KEY", raising=False) | ||
|
|
||
| client = get_authenticated_httpx_client() | ||
|
|
||
| assert client is not None | ||
| assert "Authorization" not in client.headers | ||
|
|
||
| def test_get_authenticated_httpx_client_custom_timeout(self, monkeypatch): | ||
| """Test client respects custom timeout.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-key") | ||
|
|
||
| client = get_authenticated_httpx_client(timeout=60.0) | ||
|
|
||
| assert client is not None | ||
| assert client.timeout.read == 60.0 | ||
|
|
||
| def test_get_authenticated_httpx_client_default_timeout(self, monkeypatch): | ||
| """Test client uses default timeout when not specified.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-key") | ||
|
|
||
| client = get_authenticated_httpx_client() | ||
|
|
||
| assert client is not None | ||
| assert client.timeout.read == 30.0 | ||
|
|
||
| def test_get_authenticated_httpx_client_timeout_none_uses_default( | ||
| self, monkeypatch | ||
| ): | ||
| """Test client uses default timeout when explicitly passed None.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-key") | ||
|
|
||
| client = get_authenticated_httpx_client(timeout=None) | ||
|
|
||
| assert client is not None | ||
| assert client.timeout.read == 30.0 | ||
|
|
||
| def test_get_authenticated_httpx_client_empty_api_key_no_header(self, monkeypatch): | ||
| """Test that empty API key doesn't add Authorization header.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "") | ||
|
|
||
| client = get_authenticated_httpx_client() | ||
|
|
||
| assert client is not None | ||
| # Empty string is falsy, so no auth header should be added | ||
| assert "Authorization" not in client.headers | ||
|
|
||
| def test_get_authenticated_httpx_client_zero_timeout(self, monkeypatch): | ||
| """Test client handles zero timeout correctly.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-key") | ||
|
|
||
| client = get_authenticated_httpx_client(timeout=0.0) | ||
|
|
||
| assert client is not None | ||
| assert client.timeout.read == 0.0 | ||
|
|
||
|
|
||
| class TestGetAuthenticatedRequestsSession: | ||
| """Test the get_authenticated_requests_session utility function.""" | ||
|
|
||
| def test_get_authenticated_requests_session_with_api_key(self, monkeypatch): | ||
| """Test session includes auth header when API key is set.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-api-key-123") | ||
|
|
||
| session = get_authenticated_requests_session() | ||
|
|
||
| assert session is not None | ||
| assert "Authorization" in session.headers | ||
| assert session.headers["Authorization"] == "Bearer test-api-key-123" | ||
| session.close() | ||
|
|
||
| def test_get_authenticated_requests_session_without_api_key(self, monkeypatch): | ||
| """Test session works without API key (no auth header).""" | ||
| monkeypatch.delenv("RUNPOD_API_KEY", raising=False) | ||
|
|
||
| session = get_authenticated_requests_session() | ||
|
|
||
| assert session is not None | ||
| assert "Authorization" not in session.headers | ||
| session.close() | ||
|
|
||
| def test_get_authenticated_requests_session_empty_api_key_no_header( | ||
| self, monkeypatch | ||
| ): | ||
| """Test that empty API key doesn't add Authorization header.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "") | ||
|
|
||
| session = get_authenticated_requests_session() | ||
|
|
||
| assert session is not None | ||
| # Empty string is falsy, so no auth header should be added | ||
| assert "Authorization" not in session.headers | ||
| session.close() | ||
|
|
||
| def test_get_authenticated_requests_session_is_valid_session(self, monkeypatch): | ||
| """Test returned object is a valid requests.Session.""" | ||
| monkeypatch.setenv("RUNPOD_API_KEY", "test-key") | ||
|
|
||
| session = get_authenticated_requests_session() | ||
|
|
||
| assert isinstance(session, requests.Session) | ||
| session.close() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function signature introduces a breaking change by reordering parameters. The
tokenparameter was previously in position 2 but is now in position 4 (afterbase_entry_cmd). This will break any existing calls that use positional arguments likeupdate_system_dependencies(template_id, token, deps). Consider deprecating this function and creating a new one, or keeping the original parameter order while adding a deprecation warning for the token parameter.