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
549 changes: 223 additions & 326 deletions README.md

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions backend/.tes_instances
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
# Lines starting with # are comments

# ELIXIR Cloud TES Instances
Funnel/OpenPBS @ ELIXIR-CZ,https://funnel.cloud.e-infra.cz/
Funnel/Slurm @ ELIXIR-FI,https://vm4816.kaj.pouta.csc.fi/
TESK/Kubernetes @ ELIXIR-CZ (Prod),https://tesk-prod.cloud.e-infra.cz/
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/
Funnel/OpenPBS @ ELIXIR-CZ,https://funnel.cloud.e-infra.cz
Poiesis @ ELIXIR-CZ,https://poiesis.dyn.cloud.e-infra.cz/
TESK @ ELIXIR-FI,https://csc-tesk-noauth.rahtiapp.fi
Funnel/Slurm @ ELIXIR-FI,https://fip-86-50-228-254.kaj.poutavm.fi/
Poiesis @ ELIXIR-FI,https://poiesis.rahtiapp.fi/
Test Poiesis @ ELIXIR-FI,https://test-poiesis.rahtiapp.fi/
Funnel/Slurm @ BIH,https://spe4hd.tes.bihealth.org
TESK @ ELIXIR-GR,https://tesk-eu.hypatia-comp.athenarc.gr

# Local Development
Local TES,http://localhost:8080
4 changes: 1 addition & 3 deletions backend/Dockerfile.production
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
FLASK_ENV=production

RUN groupadd --gid 1000 appuser && \
useradd --uid 1000 --gid appuser --shell /
useradd --uid 1000 --gid appuser --no-create-home --shell /usr/sbin/nologin appuser
WORKDIR /app
RUN apt-get update && apt-get install -y \
curl \
Expand All @@ -18,8 +18,6 @@ RUN pip install --no-cache-dir --upgrade pip && \
COPY . .
RUN mkdir -p uploads logs && \
chown -R appuser:appuser /app
COPY tes_instance_locations.json .
COPY .tes_instances .
USER appuser
VOLUME ["/app/uploads"]
EXPOSE 8000
Expand Down
59 changes: 48 additions & 11 deletions backend/services/tes_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,29 @@ def fetch_tes_status(instance):

def get_service_info(tes_url):
"""Get service info from a TES instance with multiple endpoint attempts"""
try:
try:
tes_url = (tes_url or "").rstrip("/")
credentials = get_instance_credentials("unknown", tes_url)
headers = {
'Accept': 'application/json',
'User-Agent': 'TES-Dashboard/1.0'
}
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'])

endpoints_to_try = [
f"{tes_url}/ga4gh/tes/v1/service-info",
f"{tes_url}/v1/tasks",
f"{tes_url}/v1/service-info",
f"{tes_url}/tasks/v1/service-info",
f"{tes_url}/service-info",
f"{tes_url}/api/service-info",
f"{tes_url}/api/v1/service-info",
f"{tes_url}/v1/tasks",
f"{tes_url}/tasks/v1/tasks",
]
Comment thread
vschnei marked this conversation as resolved.

last_error = None
Expand All @@ -153,30 +169,51 @@ def get_service_info(tes_url):
response = requests.get(
endpoint,
timeout=10,
headers={
'Accept': 'application/json',
'User-Agent': 'TES-Dashboard/1.0'
},
headers=headers,
auth=auth,
verify=True
)

print(f"📊 Response status: {response.status_code}")

if response.status_code == 200:
try:
service_info = response.json()
payload = response.json()
if endpoint.endswith('/v1/tasks'):
print(f"✅ TES API reachable via {endpoint}; building synthetic service info")
return {
'name': f"TES Service @ {tes_url}",
'id': tes_url,
'organization': {
'name': 'Unknown',
'url': tes_url
},
'description': 'TES instance reachable, service-info endpoint not exposed at common paths.',
'type': {
'group': 'ga4gh',
'artifact': 'tes',
'version': 'Unknown'
},
'contactUrl': 'Unknown',
'documentationUrl': 'Unknown',
'storage': ['Unknown'],
'version': payload.get('version', 'Unknown') if isinstance(payload, dict) else 'Unknown',
'message': 'TES endpoint is reachable; service metadata is limited.',
'timestamp': datetime.now(timezone.utc).isoformat()
}

print(f"✅ Successfully got service info from {endpoint}")
return service_info
return payload
except ValueError as json_error:
print(f"⚠️ Invalid JSON response: {json_error}")
last_error = f"Invalid JSON: {json_error}"
continue

elif response.status_code == 403:
elif response.status_code in (401, 403):
print(f"🔒 Endpoint {endpoint} requires authentication")
auth_required = True
last_error = "Authentication required"
break
last_error = "Authentication required or credentials rejected"
continue

else:
print(f"⚠️ Status {response.status_code} from {endpoint}")
Expand Down
130 changes: 87 additions & 43 deletions backend/tes_instance_locations.json
Original file line number Diff line number Diff line change
@@ -1,62 +1,106 @@
[
{
"id": "elixir-cz",
"name": "TESK Production",
"url": "https://tesk-prod.cloud.e-infra.cz",
"country": "Czech Republic",
"lat": 49.75,
"lng": 15.5,
"lon": 15.5,
"status": "healthy",
"description": "Primary TESK production instance",
"id": "elixir:cz:1",
"name": "Funnel/OpenPBS @ ELIXIR-CZ",
"url": "https://funnel.cloud.e-infra.cz",
"country": "Czechia",
"lat": 50.0755,
"lng": 14.4378,
"lon": 14.4378,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "Funnel with OpenPBS backend",
"version": "1.1",
"region": "EU-Central"
},
{
"id": "elixir:cz:2",
"name": "Poiesis @ ELIXIR-CZ",
"url": "https://poiesis.dyn.cloud.e-infra.cz/",
"country": "Czechia",
"lat": 50.0755,
"lng": 14.4378,
"lon": 14.4378,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "Poiesis TES instance",
"version": "1.1",
"region": "EU-Central"
},
{
"id": "elixir-fi",
"name": "TESK/OpenShift @ ELIXIR-FI",
"url": "https://csc-tesk-noauth.rahtiapp.fi/ga4gh/tes",
"id": "elixir:fi:1",
"name": "TESK @ ELIXIR-FI",
"url": "https://csc-tesk-noauth.rahtiapp.fi",
"country": "Finland",
"lat": 60.1699,
"lng": 24.9384,
"lon": 24.9384,
"status": "healthy",
"description": "OpenShift-based TESK instance",
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "TESK instance at CSC",
"version": "1.0",
"region": "EU-North"
},
{
"id": "elixir-gr",
"name": "TESK/Kubernetes @ ELIXIR-GR",
"url": "https://tesk.c3g.calculquebec.ca",
"country": "Greece",
"lat": 37.9838,
"lng": 23.7275,
"lon": 23.7275,
"status": "healthy",
"description": "Kubernetes-based TESK instance",
"region": "EU-South"
"id": "elixir:fi:2",
"name": "Funnel/Slurm @ ELIXIR-FI",
"url": "https://fip-86-50-228-254.kaj.poutavm.fi/",
"country": "Finland",
"lat": 60.1699,
"lng": 24.9384,
"lon": 24.9384,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "Funnel with Slurm backend",
"version": "unknown",
"region": "EU-North"
},
{
"id": "funnel-cz",
"name": "Funnel/OpenPBS @ ELIXIR-CZ",
"url": "https://funnel.cloud.e-infra.cz",
"country": "Czech Republic",
"lat": 50.0755,
"lng": 14.4378,
"lon": 14.4378,
"status": "healthy",
"description": "Funnel with OpenPBS backend",
"id": "elixir:fi:3",
"name": "Poiesis @ ELIXIR-FI",
"url": "https://poiesis.rahtiapp.fi/",
"country": "Finland",
"lat": 60.1699,
"lng": 24.9384,
"lon": 24.9384,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "Poiesis TES instance",
"version": "1.1",
"region": "EU-North"
},
{
"id": "elixir:fi:4",
"name": "Test Poiesis @ ELIXIR-FI",
"url": "https://test-poiesis.rahtiapp.fi/",
"country": "Finland",
"lat": 60.1699,
"lng": 24.9384,
"lon": 24.9384,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "Test Poiesis instance",
"version": "1.1",
"region": "EU-North"
},
{
"id": "elixir:de:1",
"name": "Funnel/Slurm @ BIH",
"url": "https://spe4hd.tes.bihealth.org",
"country": "Germany",
"lat": 52.52,
"lng": 13.40,
"lon": 13.40,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "Funnel with Slurm backend",
"version": "1.0",
"region": "EU-Central"
},
{
"id": "local-tes",
"name": "Local TES",
"url": "http://localhost:8080",
"country": "Local",
"lat": 0,
"lng": 0,
"lon": 0,
"status": "unknown",
"description": "Local development TES instance",
"region": "Local"
"id": "elixir:gr:1",
"name": "TESK @ ELIXIR-GR",
"url": "https://tesk-eu.hypatia-comp.athenarc.gr",
"country": "Greece",
"lat": 37.9838,
"lng": 23.7275,
"lon": 23.7275,
"status": "online",
Comment thread
vschnei marked this conversation as resolved.
"description": "TESK instance",
"version": "1.0",
"region": "EU-South"
}
]
32 changes: 24 additions & 8 deletions backend/utils/auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,37 @@
from config import FUNNEL_SERVER_USER, FUNNEL_SERVER_PASSWORD, TES_TOKEN

def get_instance_credentials(instance_name, instance_url):
instance_url = (instance_url or '').rstrip('/')
default_user = FUNNEL_SERVER_USER
default_pass = FUNNEL_SERVER_PASSWORD
default_token = TES_TOKEN
Comment thread
vschnei marked this conversation as resolved.

if 'tesk-prod.cloud.e-infra.cz' in instance_url:
if 'funnel.cloud.e-infra.cz' in instance_url:
return {
'user': os.getenv('TESK_PROD_USER', default_user),
'password': os.getenv('TESK_PROD_PASSWORD', default_pass),
'token': os.getenv('TESK_PROD_TOKEN', default_token)
'user': os.getenv('FUNNEL_CZ_USER', default_user),
'password': os.getenv('FUNNEL_CZ_PASSWORD', default_pass),
'token': None
}
elif 'tesk-na.cloud.e-infra.cz' in instance_url:

elif 'fip-86-50-228-254.kaj.poutavm.fi' in instance_url:
return {
'user': os.getenv('FUNNEL_FI_USER', default_user),
'password': os.getenv('FUNNEL_FI_PASSWORD', default_pass),
'token': None
}

elif 'tesk-eu.hypatia-comp.athenarc.gr' in instance_url:
return {
'user': os.getenv('TESK_GR_USER', default_user),
'password': os.getenv('TESK_GR_PASSWORD', default_pass),
'token': None
}

elif 'spe4hd.tes.bihealth.org' in instance_url:
return {
'user': os.getenv('TESK_NA_USER', default_user),
'password': os.getenv('TESK_NA_PASSWORD', default_pass),
'token': os.getenv('TESK_NA_TOKEN', default_token)
'user': None,
'password': None,
'token': os.getenv('BIHEALTH_TOKEN', default_token)
}
else:
return {
Expand Down
39 changes: 39 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
services:
tes-dashboard-backend-service:
build:
context: ./backend
dockerfile: Dockerfile.production
container_name: tes-dashboard-backend
environment:
PORT: 8000
FLASK_ENV: production
FLASK_DEBUG: "false"
Comment on lines +7 to +10
ports:
- "8000:8000"
volumes:
- ./backend/uploads:/app/uploads
- ./backend/tes_instance_locations.json:/app/tes_instance_locations.json
- ./backend/.tes_instances:/app/.tes_instances
extra_hosts:
- "spe4hd.tes.bihealth.org:194.94.4.48"
Comment thread
vschnei marked this conversation as resolved.
Comment thread
vschnei marked this conversation as resolved.
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health', timeout=5)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped

tes-dashboard-frontend-service:
build:
context: ./frontend
dockerfile: Dockerfile
args:
REACT_APP_API_URL: ""
container_name: tes-dashboard-frontend
depends_on:
tes-dashboard-backend-service:
condition: service_healthy
ports:
- "3000:80"
restart: unless-stopped
4 changes: 4 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ RUN npm ci --only=production
# Copy source code
COPY . .

# Accept API URL build argument (empty = use nginx proxy)
ARG REACT_APP_API_URL=""
ENV REACT_APP_API_URL=$REACT_APP_API_URL

# Build the application
RUN npm run build

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/api.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios';

const getApiBaseUrl = () => {
if (process.env.REACT_APP_API_URL) {
if (process.env.REACT_APP_API_URL !== undefined && process.env.REACT_APP_API_URL !== null) {
return process.env.REACT_APP_API_URL;
}

Expand Down
Loading