diff --git a/backend/.tes_instances b/backend/.tes_instances
index 2bfea85..3396567 100644
--- a/backend/.tes_instances
+++ b/backend/.tes_instances
@@ -10,7 +10,6 @@ TESK/Kubernetes @ ELIXIR-CZ (NA),https://tesk-na.cloud.e-infra.cz/
TESK/Kubernetes @ ELIXIR-DE,https://tesk.elixir-cloud.bi.denbi.de/
TESK/Kubernetes @ ELIXIR-GR,https://tesk-eu.hypatia-comp.athenarc.gr/
TESK/OpenShift @ ELIXIR-FI,https://csc-tesk-noauth.rahtiapp.fi/
-TESK North America,https://tesk-na.cloud.e-infra.cz/
# Local Development
Local TES,http://localhost:8080
diff --git a/backend/routes/instances.py b/backend/routes/instances.py
index 815c399..4b2247a 100644
--- a/backend/routes/instances.py
+++ b/backend/routes/instances.py
@@ -28,6 +28,27 @@ def get_healthy_instances_route():
print(f"❌ Error in get_healthy_instances: {str(e)}")
return jsonify({'error': str(e)}), 500
+@instances_bp.route('/api/instances-with-status', methods=['GET'])
+def get_instances_with_status():
+ """Get all TES instances with their current health status"""
+ try:
+ from utils.tes_utils import load_tes_location_data
+ instances = load_tes_location_data()
+
+ # Fetch status for all instances in parallel
+ with ThreadPoolExecutor(max_workers=8) as pool:
+ results = list(pool.map(fetch_tes_status, instances))
+
+ return jsonify({
+ 'instances': results,
+ 'count': len(results),
+ 'last_updated': datetime.now(timezone.utc).isoformat()
+ })
+
+ except Exception as e:
+ print(f"Error in get_instances_with_status: {str(e)}")
+ return jsonify({'error': str(e)}), 500
+
@instances_bp.route('/api/tes_locations', methods=['GET'])
def tes_locations():
from utils.tes_utils import load_tes_location_data
diff --git a/backend/routes/tasks.py b/backend/routes/tasks.py
index ee5c77c..7c0af59 100644
--- a/backend/routes/tasks.py
+++ b/backend/routes/tasks.py
@@ -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)}")
credentials = get_instance_credentials(tes_name, tes_url)
headers = {
@@ -301,7 +346,7 @@ def submit_task():
else:
error_type_map = {
400: {'error_type': 'bad_request', 'error_code': 'BAD_REQUEST', 'reason': 'The task specification is invalid or malformed'},
- 401: {'error_type': 'unauthorized', 'error_code': 'UNAUTHORIZED', 'reason': 'Authentication required or credentials are invalid'},
+ 401: {'error_type': 'unauthorized', 'error_code': 'UNAUTHORIZED', 'reason': 'Authentication required. Please configure TESK_PROD_TOKEN or TESK_PROD_USER/TESK_PROD_PASSWORD environment variables for this instance.'},
403: {'error_type': 'forbidden', 'error_code': 'FORBIDDEN', 'reason': 'You do not have permission to submit tasks to this instance'},
404: {'error_type': 'not_found', 'error_code': 'NOT_FOUND', 'reason': 'The TES endpoint was not found. Check if the URL is correct.'},
408: {'error_type': 'timeout', 'error_code': 'TIMEOUT', 'reason': 'The request timed out. The TES instance may be overloaded.'},
@@ -319,12 +364,21 @@ def submit_task():
})
error_msg = f'TES submission failed with status {response.status_code}'
+ 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}")
if error_data.get('message'):
error_msg = error_data.get('message')
elif error_data.get('error'):
error_msg = error_data.get('error')
+ elif error_data.get('detail'):
+ error_msg = error_data.get('detail')
+ elif error_data.get('title'):
+ error_msg = error_data.get('title')
else:
error_msg = f'{error_msg}: {str(error_data)}'
except:
diff --git a/backend/services/tes_service.py b/backend/services/tes_service.py
index c65f2a3..2f656c6 100644
--- a/backend/services/tes_service.py
+++ b/backend/services/tes_service.py
@@ -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:
+ instance_name = instance.get("name", "")
+ credentials = get_instance_credentials(instance_name, tes_base_url)
+
+ # Test tasks endpoint with credentials (if available)
+ headers = {'Accept': 'application/json'}
+ auth = None
+ if credentials.get('token'):
+ headers['Authorization'] = f"Bearer {credentials['token']}"
+ elif credentials.get('user') and credentials.get('password'):
+ auth = (credentials['user'], credentials['password'])
+
+ # Try to list tasks (with view=MINIMAL to reduce payload)
+ tasks_response = requests.get(
+ f"{tes_base_url}/ga4gh/tes/v1/tasks?view=MINIMAL",
+ headers=headers,
+ auth=auth,
+ timeout=5
+ )
+
+ # If tasks endpoint returns 401/403, mark as unhealthy (auth required but not configured)
+ if tasks_response.status_code in [401, 403]:
+ print(f"⚠️ {instance.get('name')} tasks endpoint requires authentication (status {tasks_response.status_code})")
+ status = "unhealthy"
+ else:
+ # Tasks endpoint is accessible (200) or returns other non-auth error
+ status = "healthy"
+ except Exception as tasks_error:
+ print(f"⚠️ Could not check tasks endpoint for {instance.get('name')}: {tasks_error}")
+ # If we can't check tasks endpoint, assume healthy based on service-info
+ status = "healthy"
+
version = ""
try:
version = r.json().get("version", "")
diff --git a/frontend/src/hooks/useInstances.js b/frontend/src/hooks/useInstances.js
index ae9f8bd..de3d9d1 100644
--- a/frontend/src/hooks/useInstances.js
+++ b/frontend/src/hooks/useInstances.js
@@ -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
+ });
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,
diff --git a/frontend/src/pages/SubmitTask.js b/frontend/src/pages/SubmitTask.js
index 162cbf5..c793ffe 100644
--- a/frontend/src/pages/SubmitTask.js
+++ b/frontend/src/pages/SubmitTask.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import { testConnection } from '../services/api';
@@ -196,11 +196,33 @@ const SubmitTask = () => {
const {
instances,
+ allInstances,
loading: instancesLoading,
error: instancesError,
refresh: refreshInstances
} = useInstances();
+ // Helper function to get status badge
+ const getStatusBadge = (status) => {
+ return status === 'healthy' ? '✅' : '❌';
+ };
+
+ useEffect(() => {
+ // Auto-select first healthy instance if no instance is selected
+ if (instances.length > 0 && !formData.tes_instance) {
+ // Try to find a healthy instance from allInstances (if available)
+ const healthyInstance = (allInstances.length > 0 ? allInstances : instances).find(
+ instance => instance.status === 'healthy'
+ );
+
+ // Only set default if a healthy instance exists
+ if (healthyInstance) {
+ setFormData(prev => ({ ...prev, tes_instance: healthyInstance.url }));
+ }
+ }
+
+ }, [instances, allInstances]);
+
const handleTestConnection = async () => {
try {
setTestingConnection(true);
@@ -238,46 +260,53 @@ const SubmitTask = () => {
};
const getDemoTaskData = (demoType = 'basic') => {
- const defaultTesInstance = instances.length > 0
- ? instances[0].url
- : 'https://csc-tesk-noauth.rahtiapp.fi/v1/tasks';
+ // Use first healthy instance as default for demos
+ let defaultTesInstance = '';
+
+ if (instances.length > 0) {
+ const healthyInstance = (allInstances.length > 0 ? allInstances : instances).find(
+ instance => instance.status === 'healthy'
+ );
+ // Only use an instance if it's healthy, otherwise leave empty
+ defaultTesInstance = healthyInstance ? healthyInstance.url : '';
+ }
const demoTasks = {
basic: {
tes_instance: defaultTesInstance,
task_name: 'Demo Hello World Task',
- docker_image: 'ubuntu:20.04',
- command: 'echo "Hello from TES Demo Task!" && echo "Current time: $(date)" && echo "System info: $(uname -a)" && echo "Task completed successfully"',
+ docker_image: 'alpine:latest',
+ command: 'echo "Hello from TES!" && date && uname -a',
input_url: '',
output_url: '',
cpu_cores: '1',
ram_gb: '1',
- disk_gb: '5',
- description: 'A simple demo task that prints system information and a hello message. Safe to run and completes quickly for testing purposes.'
+ disk_gb: '1',
+ description: 'Simple demo task using Alpine Linux (5MB). Note: If you see SYSTEM_ERROR, the TES instance may be experiencing infrastructure issues. Try a different instance or wait a few minutes.'
},
python: {
tes_instance: defaultTesInstance,
task_name: 'Demo Python Script Task',
- docker_image: 'python:3.9-slim',
- command: 'python3 -c "import sys; import datetime; print(f\'Hello from Python {sys.version}\'); print(f\'Current time: {datetime.datetime.now()}\'); print(\'Demo task completed successfully!\')"',
+ docker_image: 'python:3.11-alpine',
+ command: 'python3 -c "import sys; import datetime; print(sys.version); print(datetime.datetime.now())"',
input_url: '',
output_url: '',
cpu_cores: '1',
ram_gb: '1',
- disk_gb: '5',
- description: 'A Python demo task that prints version info and timestamp using a Python container.'
+ disk_gb: '1',
+ description: 'Python demo using Alpine-based image (51MB). Faster than standard Python images.'
},
fileops: {
tes_instance: defaultTesInstance,
task_name: 'Demo File Operations Task',
- docker_image: 'ubuntu:20.04',
- command: 'echo "Creating demo files..." && echo "Hello World" > /tmp/output.txt && echo "File contents:" && cat /tmp/output.txt && ls -la /tmp/',
+ docker_image: 'alpine:latest',
+ command: 'echo "Hello World" > /tmp/demo.txt && cat /tmp/demo.txt && ls -lh /tmp/demo.txt',
input_url: '',
output_url: '',
cpu_cores: '1',
ram_gb: '1',
- disk_gb: '5',
- description: 'A demo task that creates and reads files, demonstrating basic file operations within the container.'
+ disk_gb: '1',
+ description: 'Demonstrates file creation and reading in Alpine Linux.'
}
};
@@ -287,15 +316,7 @@ const SubmitTask = () => {
const handleRunDemo = (demoType = 'basic') => {
const demoData = getDemoTaskData(demoType);
setFormData(demoData);
- setError(null);
-
- const taskNames = {
- basic: 'Basic Hello World',
- python: 'Python Script',
- fileops: 'File Operations'
- };
-
- alert(`${taskNames[demoType] || 'Demo'} task data loaded! Review the form and click "Submit Task" when ready.`);
+ setError(null);
};
const handleSubmit = async (e) => {
@@ -329,12 +350,7 @@ const SubmitTask = () => {
const result = await taskService.submitTask(submitData);
console.log('Task submission result:', result);
-
- if (result && result.message) {
- alert(`Success: ${result.message}`);
- } else {
- alert('Task submitted successfully!');
- }
+
navigate('/tasks');
} catch (err) {
console.error('Task submission error:', err);
@@ -440,11 +456,23 @@ const SubmitTask = () => {
required
>
- {instances.map((instance, index) => (
-
- ))}
+ {(allInstances.length > 0 ? allInstances : instances)
+ .slice()
+ .sort((a, b) => {
+ // Sort by status: healthy (reachable without auth) first, then others
+ // Note: Backend marks instances requiring auth as 'unhealthy'
+ if (a.status === 'healthy' && b.status !== 'healthy') return -1;
+ if (a.status !== 'healthy' && b.status === 'healthy') return 1;
+ return 0;
+ })
+ .map((instance) => (
+
+ ))}
diff --git a/frontend/src/pages/Utilities.js b/frontend/src/pages/Utilities.js
index 52824bf..977bb3b 100644
--- a/frontend/src/pages/Utilities.js
+++ b/frontend/src/pages/Utilities.js
@@ -406,7 +406,7 @@ const Utilities = () => {
));
const serviceInfo = response.data;
- let successMessage = `✅ Connection Test Successful!\n\nInstance: ${instanceName}\nResponse Time: ${responseTime}ms`;
+ let successMessage = `Connection Test Successful!\n\nInstance: ${instanceName}\nResponse Time: ${responseTime}ms`;
if (serviceInfo && serviceInfo.name) {
successMessage += `\nService Name: ${serviceInfo.name}`;
@@ -417,7 +417,7 @@ const Utilities = () => {
alert(successMessage);
} catch (error) {
- let errorMessage = `❌ Connection Test Failed\n\nInstance: ${instanceName}\nURL: ${url}\n\n`;
+ let errorMessage = `Connection Test Failed\n\nInstance: ${instanceName}\nURL: ${url}\n\n`;
let errorReason = '';
let errorCode = '';
let errorType = '';
diff --git a/frontend/src/services/instanceService.js b/frontend/src/services/instanceService.js
index 19b8dde..3f75910 100644
--- a/frontend/src/services/instanceService.js
+++ b/frontend/src/services/instanceService.js
@@ -3,6 +3,7 @@ import api from './api';
class InstanceService {
constructor() {
this.healthyInstances = [];
+ this.allInstancesWithStatus = [];
this.loading = false;
this.error = null;
this.lastUpdate = null;
@@ -24,6 +25,7 @@ class InstanceService {
try {
callback({
instances: this.healthyInstances,
+ allInstances: this.allInstancesWithStatus,
loading: this.loading,
error: this.error,
lastUpdate: this.lastUpdate
@@ -43,6 +45,15 @@ class InstanceService {
};
}
+ getAllInstancesWithStatus() {
+ return {
+ instances: this.allInstancesWithStatus,
+ loading: this.loading,
+ error: this.error,
+ lastUpdate: this.lastUpdate
+ };
+ }
+
async fetchHealthyInstances() {
try {
const isInitialLoad = this.healthyInstances.length === 0;
@@ -50,24 +61,31 @@ class InstanceService {
if (isInitialLoad) {
this.loading = true;
this.notifyListeners();
- console.log('🔄 Loading initial healthy TES instances...');
+ console.log('🔄 Loading TES instances with status...');
} else {
- console.log('🔄 Refreshing cached healthy TES instances...');
+ console.log('🔄 Refreshing TES instances with status...');
}
- const response = await api.get('/api/healthy-instances', {
- timeout: 5000
+ const response = await api.get('/api/instances-with-status', {
+ timeout: 10000
});
const data = response.data;
- this.healthyInstances = data.instances || [];
+ const allInstances = data.instances || [];
+
+ // Filter to only healthy instances, but keep status info
+ this.healthyInstances = allInstances.filter(inst => inst.status === 'healthy');
+
+ // Store all instances (including unhealthy) for dropdown display
+ this.allInstancesWithStatus = allInstances;
+
this.lastUpdate = data.last_updated ? new Date(data.last_updated) : new Date();
this.error = null;
- console.log(`✅ Got ${this.healthyInstances.length} cached healthy TES instances (updated: ${this.lastUpdate.toLocaleTimeString()})`);
+ console.log(`✅ Got ${this.healthyInstances.length} healthy instances out of ${allInstances.length} total (updated: ${this.lastUpdate.toLocaleTimeString()})`);
} catch (err) {
- console.error('Error fetching healthy instances:', err);
+ console.error('Error fetching instances with status:', err);
if (err.code === 'ECONNABORTED') {
this.error = 'Connection timeout - using cached data';
@@ -84,11 +102,7 @@ class InstanceService {
console.log(`Using ${this.healthyInstances.length} cached healthy instances during error`);
}
} finally {
- if (this.healthyInstances.length === 0) {
- this.loading = false;
- } else {
- this.loading = false;
- }
+ this.loading = false;
this.notifyListeners();
}
}