-
Notifications
You must be signed in to change notification settings - Fork 0
feat: resolving issue #8 #9 and #10 #18
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
base: main
Are you sure you want to change the base?
Changes from all commits
1d22447
993e9b1
a789ebd
cfccbae
a63c052
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,9 +4,13 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||
| import requests | ||||||||||||||||||||||||||||||||||||||||||||||||
| import shlex | ||||||||||||||||||||||||||||||||||||||||||||||||
| from services.task_service import get_submitted_tasks, add_task, update_single_task_status | ||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.tes_utils import load_tes_instances | ||||||||||||||||||||||||||||||||||||||||||||||||
| from utils.auth_utils import get_instance_credentials | ||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| tasks_bp = Blueprint('tasks', __name__) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,9 +40,38 @@ def submit_task(): | |||||||||||||||||||||||||||||||||||||||||||||||
| tes_name = inst['name'] | ||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| command_raw = data.get('command', '') | ||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(command_raw, list): | ||||||||||||||||||||||||||||||||||||||||||||||||
| command = command_raw | ||||||||||||||||||||||||||||||||||||||||||||||||
| elif command_raw: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| shell_operators = ['&&', '||', '|', '>', '<', '>>', '2>', '&', ';', '$(', '`'] | ||||||||||||||||||||||||||||||||||||||||||||||||
| needs_shell = any(op in command_raw for op in shell_operators) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if needs_shell: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| command = ["/bin/sh", "-c", command_raw] | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"🐚 Command contains shell operators, wrapping in shell: {command}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| command = shlex.split(command_raw) | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"📝 Simple command parsed: {command}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| except ValueError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(f"Command parsing error: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| return jsonify({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'success': False, | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'error': f'Invalid command syntax: {str(e)}', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'error_type': 'bad_request', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'error_code': 'INVALID_COMMAND' | ||||||||||||||||||||||||||||||||||||||||||||||||
| }), 400 | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
| command = ['echo', 'Hello World'] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| executor = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| "image": docker_image, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "command": data.get('command') if isinstance(data.get('command'), list) else (data.get('command', '').split() if data.get('command') else ['echo', 'Hello World']), | ||||||||||||||||||||||||||||||||||||||||||||||||
| "command": command, | ||||||||||||||||||||||||||||||||||||||||||||||||
| "workdir": data.get('workdir', '/tmp') | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -100,13 +133,24 @@ def submit_task(): | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f" Trying service-info: {service_info_url}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| test_response = requests.get(service_info_url, timeout=10) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if test_response.status_code in [200, 403]: | ||||||||||||||||||||||||||||||||||||||||||||||||
| test_response = requests.get(service_info_url, timeout=10) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if test_response.status_code == 200: | ||||||||||||||||||||||||||||||||||||||||||||||||
| service_is_reachable = True | ||||||||||||||||||||||||||||||||||||||||||||||||
| working_endpoint = tasks_url | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f" ✅ Service reachable at {service_info_url} (status {test_response.status_code})") | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f" Will use tasks endpoint: {tasks_url}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||
| elif test_response.status_code in [401, 403]: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Authentication required - treat as unreachable since we can't use it | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f" 🔐 Authentication required at {service_info_url} (status {test_response.status_code})") | ||||||||||||||||||||||||||||||||||||||||||||||||
| connectivity_error_info = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'error_type': 'unauthorized', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'error_code': 'UNAUTHORIZED', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'message': 'Authentication required - TES instance requires credentials', | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'reason': 'This TES instance requires authentication. Please configure TESK_PROD_TOKEN or credentials in environment variables.' | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f" ⚠️ Service returned status {test_response.status_code}") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -199,6 +243,7 @@ def submit_task(): | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| tes_endpoint = working_endpoint | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"🚀 Submitting task to {tes_endpoint}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"📦 Task payload: {json.dumps(tes_task, indent=2)}") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"📦 Task payload: {json.dumps(tes_task, indent=2)}") | |
| logger.debug("📦 Task payload keys: %s", list(tes_task.keys())) |
Copilot
AI
Feb 12, 2026
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.
On submission errors, the code prints full TES response headers and body (Response headers, Response body). Headers/bodies can include sensitive information and can be large; this is better handled via logger.debug with redaction/truncation (and ideally only when debugging).
| print(f"❌ TES returned status {response.status_code}") | |
| print(f"Response headers: {dict(response.headers)}") | |
| print(f"Response body: {response.text[:500]}") | |
| try: | |
| error_data = response.json() | |
| print(f"Error data JSON: {error_data}") | |
| logger.debug("TES submission failed with status %s", response.status_code) | |
| logger.debug( | |
| "TES response header names: %s", | |
| list(response.headers.keys()) | |
| ) | |
| logger.debug( | |
| "TES response body (truncated): %s", | |
| (response.text[:200] + "..." if response.text and len(response.text) > 200 else response.text) | |
| ) | |
| try: | |
| error_data = response.json() | |
| logger.debug( | |
| "TES error data JSON (truncated): %s", | |
| str(error_data)[:500] + ("..." if len(str(error_data)) > 500 else "") | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -28,7 +28,46 @@ def fetch_tes_status(instance): | |||||
| r = requests.get(f"{tes_base_url}/ga4gh/tes/v1/service-info", timeout=5) | ||||||
| latency_ms = int((time.time() - start_time) * 1000) | ||||||
|
|
||||||
| status = "healthy" if r.status_code == 200 else "unhealthy" | ||||||
| # First check service-info endpoint | ||||||
| if r.status_code in [401, 403]: | ||||||
| status = "unhealthy" # Authentication required but not available | ||||||
| elif r.status_code != 200: | ||||||
| status = "unhealthy" | ||||||
| else: | ||||||
| # Service-info is accessible, but we need to check if tasks endpoint is usable | ||||||
| # Try a HEAD/OPTIONS request to tasks endpoint to see if it requires auth | ||||||
|
||||||
| # Try a HEAD/OPTIONS request to tasks endpoint to see if it requires auth | |
| # Try a request to the tasks endpoint to see if it requires auth |
Copilot
AI
Feb 12, 2026
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.
In fetch_tes_status, the instance is marked healthy for any tasks endpoint response that isn’t 401/403. This can incorrectly classify instances as healthy even when /tasks returns 404/500/405, which would still break demo submissions. Consider only marking healthy for 2xx (ideally 200) responses, and treating other status codes as unhealthy (while still keeping the special-case for 401/403).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import instanceService from '../services/instanceService'; | |
| const useInstances = () => { | ||
| const [state, setState] = useState({ | ||
| instances: [], | ||
| allInstances: [], | ||
| loading: true, | ||
| error: null, | ||
| lastUpdate: null | ||
|
|
@@ -15,7 +16,11 @@ const useInstances = () => { | |
| }; | ||
| instanceService.addListener(handleUpdate); | ||
| const initialState = instanceService.getHealthyInstances(); | ||
| setState(initialState); | ||
| const allInstancesState = instanceService.getAllInstancesWithStatus(); | ||
| setState({ | ||
| ...initialState, | ||
| allInstances: allInstancesState.instances | ||
| }); | ||
|
Comment on lines
17
to
+23
|
||
| return () => { | ||
| instanceService.removeListener(handleUpdate); | ||
| }; | ||
|
|
@@ -26,6 +31,7 @@ const useInstances = () => { | |
|
|
||
| return { | ||
| instances: state.instances, | ||
| allInstances: state.allInstances, | ||
| loading: state.loading, | ||
| error: state.error, | ||
| lastUpdate: state.lastUpdate, | ||
|
|
||
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 new '/api/instances-with-status' endpoint is not included in the cache patterns in middleware_config.py. This endpoint performs real-time health checks on all TES instances which can be expensive (lines 38-40 show parallel HTTP requests to multiple endpoints). The endpoint should be added to the cache_patterns list to avoid repeated expensive operations, similar to '/api/instances' and '/api/tes_locations' which are already cached.