Skip to content

Commit 413d65d

Browse files
working frontend and CI with frontend tests
1 parent 74ba7bf commit 413d65d

File tree

6 files changed

+79
-5
lines changed

6 files changed

+79
-5
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,14 @@ jobs:
273273
echo "Launching end-to-end tests"
274274
uv run pytest tests/tests_end_to_end/test_from_project_creation_to_model_predict.py -v --tb=long
275275
276+
- name: Install Playwright browser (Chromium)
277+
run: uv run playwright install --with-deps chromium
278+
279+
- name: Run frontend e2e tests
280+
run: |
281+
echo "Launching frontend e2e tests"
282+
uv run pytest tests/tests_end_to_end/test_frontend_e2e.py -v --tb=long
283+
276284
- name: Collect logs on failure
277285
if: failure()
278286
run: |

backend/api/health_check.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
This module provides a health check endpoint for the API.
44
"""
55

6+
import os
7+
8+
import httpx
69
from fastapi import APIRouter
10+
from loguru import logger
711
from pydantic import BaseModel
812

913
router = APIRouter()
@@ -25,3 +29,28 @@ def health_check():
2529
A dictionary with the status of the API.
2630
"""
2731
return HealthCheck(status="OK")
32+
33+
34+
@router.get("/storage")
35+
def storage_health_check():
36+
"""Check MinIO/S3 storage reachability from the backend.
37+
38+
Uses the MinIO live health endpoint so the browser never has to reach
39+
an internal cluster URL directly.
40+
41+
Returns
42+
-------
43+
dict
44+
{"status": "ok"} or {"status": "error", "detail": "..."}
45+
"""
46+
s3_url = os.environ.get("MLFLOW_S3_ENDPOINT_URL", "")
47+
if not s3_url:
48+
return {"status": "error", "detail": "MLFLOW_S3_ENDPOINT_URL not configured"}
49+
try:
50+
response = httpx.get(f"{s3_url.rstrip('/')}/minio/health/live", timeout=3.0)
51+
if response.status_code == 200:
52+
return {"status": "ok"}
53+
return {"status": "error", "detail": f"HTTP {response.status_code}"}
54+
except Exception as e:
55+
logger.warning(f"Storage health check failed: {e}")
56+
return {"status": "error", "detail": str(e)}

backend/api/projects_routes.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from backend.domain.ports.user_handler import UserHandler
1414
from backend.domain.use_cases import user_usecases
1515
from backend.domain.use_cases.auth_usecases import get_current_user, get_user_adapter
16+
from backend.domain.use_cases.deployed_models import get_registry_status_for_project
1617
from backend.domain.use_cases.governance_usecases import (
1718
download_project_models_governance_information,
1819
return_project_models_governance_information,
@@ -157,8 +158,8 @@ def governance_route(
157158
try:
158159
project_governance = return_project_models_governance_information(project_name, registry)
159160
except Exception as e:
160-
logger.warning(f"Could not fetch governance data for project {project_name}: {e}")
161-
raise HTTPException(status_code=503, detail=f"MLflow registry unreachable for project '{project_name}'")
161+
logger.exception(f"Error fetching governance data for project {project_name}")
162+
raise HTTPException(status_code=500, detail=str(e))
162163
return JSONResponse(content={"project_governance": project_governance}, media_type="application/json")
163164

164165

@@ -211,3 +212,13 @@ def route_change_user_role_for_project(
211212
)
212213
success = user_usecases.change_user_role_for_project(email, project_name, role, user_adapter)
213214
return JSONResponse(content={"status": success}, media_type="application/json")
215+
216+
217+
@router.get("/{project_name}/registry_status")
218+
def registry_status_route(
219+
project_name: str,
220+
current_user: dict = Depends(get_current_user),
221+
):
222+
"""Return the K8s deployment status of the MLflow registry for a project."""
223+
status = get_registry_status_for_project(project_name)
224+
return {"status": status}

backend/domain/use_cases/deployed_models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from backend.domain.entities.model_deployment import ModelDeployment
44
from backend.infrastructure.k8s_deployment_cluster_adapter import K8SDeploymentClusterAdapter
55
from backend.infrastructure.k8s_registry_deployment_adapter import K8SRegistryDeployment
6+
from backend.utils import sanitize_project_name
67

78

89
def list_deployed_models_with_status_for_a_project(project_name: str) -> list[str]:
@@ -13,6 +14,25 @@ def list_deployed_models_with_status_for_a_project(project_name: str) -> list[st
1314
return deployed_models_json
1415

1516

17+
def get_registry_status_for_project(project_name: str) -> str:
18+
"""Return the K8s deployment status of the MLflow registry for a project.
19+
20+
Returns one of: 'running', 'pending', 'error', 'not_found'.
21+
"""
22+
k8s = K8SDeploymentClusterAdapter()
23+
namespace = sanitize_project_name(project_name)
24+
try:
25+
deployments = k8s.apps_api_instance.list_namespaced_deployment(
26+
namespace=namespace, label_selector="type=model_registry"
27+
)
28+
if not deployments.items:
29+
return "not_found"
30+
return k8s._resolve_deployment_status(deployments.items[0].status)
31+
except Exception as e:
32+
logger.warning(f"Could not get registry status for {project_name}: {e}")
33+
return "error"
34+
35+
1636
def _remove_project_namespace(project_name: str) -> None:
1737
k8s_deployment = K8SRegistryDeployment(project_name)
1838
k8s_deployment.delete_namespace()

backend/domain/use_cases/governance_usecases.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ def _extract_model_artifacts(
2828

2929
def _filter_events_for_model(project_events: list, model_name: str, version: str):
3030
model_events = []
31-
for event in project_events:
32-
event_entity = event.get("entity").replace("'", '"')
33-
event_entity = json.loads(event_entity)
31+
for event in project_events or []:
32+
raw_entity = event.get("entity")
33+
if not raw_entity:
34+
continue
35+
try:
36+
event_entity = json.loads(raw_entity.replace("'", '"'))
37+
except (json.JSONDecodeError, AttributeError):
38+
continue
3439
if (
3540
"model_name" in event_entity
3641
and event_entity["model_name"] == model_name

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dev = [
5454
"pytest>=8.3.4",
5555
"pytest-asyncio>=0.25.3",
5656
"pytest-mock>=3.14.0",
57+
"pytest-playwright",
5758
]
5859
notebooks = [
5960
"pyomo>=6.9.0",

0 commit comments

Comments
 (0)