diff --git a/services/docker-compose.local.yml b/services/docker-compose.local.yml index 37bbb3e9b05..872b3ea503f 100644 --- a/services/docker-compose.local.yml +++ b/services/docker-compose.local.yml @@ -100,6 +100,7 @@ services: environment: <<: *common_environment DYNAMIC_SCHEDULER_REMOTE_DEBUGGING_PORT : 3000 + DYNAMIC_SCHEDULER_UI_MOUNT_PATH: / ports: - "8012:8000" - "3016:3000" diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py index 9e689c86023..50bb82fc0f3 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_setup.py @@ -13,7 +13,7 @@ def setup_frontend(app: FastAPI) -> None: nicegui.ui.run_with( app, - mount_path="/", + mount_path=settings.DYNAMIC_SCHEDULER_UI_MOUNT_PATH, storage_secret=settings.DYNAMIC_SCHEDULER_UI_STORAGE_SECRET.get_secret_value(), ) set_parent_app(app) diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_utils.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_utils.py index 6e890b8b8fe..6d3f61c31fc 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_utils.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/_utils.py @@ -1,6 +1,8 @@ import nicegui from fastapi import FastAPI +from ...core.settings import ApplicationSettings + def set_parent_app(parent_app: FastAPI) -> None: nicegui.app.state.parent_app = parent_app @@ -9,3 +11,9 @@ def set_parent_app(parent_app: FastAPI) -> None: def get_parent_app(app: FastAPI) -> FastAPI: parent_app: FastAPI = app.state.parent_app return parent_app + + +def get_settings() -> ApplicationSettings: + parent_app = get_parent_app(nicegui.app) + settings: ApplicationSettings = parent_app.state.settings + return settings diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py index 5c864651427..1163328bfe7 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_index.py @@ -10,7 +10,7 @@ from ....services.service_tracker import TrackedServiceModel, get_all_tracked_services from ....services.service_tracker._models import SchedulerServiceState -from .._utils import get_parent_app +from .._utils import get_parent_app, get_settings from ._render_utils import base_page, get_iso_formatted_date router = APIRouter() @@ -70,9 +70,9 @@ def _render_buttons(node_id: NodeID, service: TrackedServiceModel) -> None: async def _stop_service() -> None: confirm_dialog.close() - await httpx.AsyncClient(timeout=10).get( - f"http://localhost:{DEFAULT_FASTAPI_PORT}/service/{node_id}:stop" - ) + + url = f"http://localhost:{DEFAULT_FASTAPI_PORT}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}service/{node_id}:stop" + await httpx.AsyncClient(timeout=10).get(f"{url}") ui.notify( f"Submitted stop request for {node_id}. Please give the service some time to stop!" diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py index b4d9327df0f..468de4aedb9 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/api/frontend/routes/_service.py @@ -14,7 +14,7 @@ from ....core.settings import ApplicationSettings from ....services.service_tracker import get_tracked_service, remove_tracked_service -from .._utils import get_parent_app +from .._utils import get_parent_app, get_settings from ._render_utils import base_page router = APIRouter() @@ -25,9 +25,9 @@ def _render_remove_from_tracking(node_id): async def remove_from_tracking(): confirm_dialog.close() - await httpx.AsyncClient(timeout=10).get( - f"http://localhost:{DEFAULT_FASTAPI_PORT}/service/{node_id}/tracker:remove" - ) + + url = f"http://localhost:{DEFAULT_FASTAPI_PORT}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}service/{node_id}/tracker:remove" + await httpx.AsyncClient(timeout=10).get(f"{url}") ui.notify(f"Service {node_id} removed from tracking") ui.navigate.to("/") diff --git a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/settings.py b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/settings.py index 9f046943344..36be9f4b587 100644 --- a/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/settings.py +++ b/services/dynamic-scheduler/src/simcore_service_dynamic_scheduler/core/settings.py @@ -95,6 +95,10 @@ class ApplicationSettings(_BaseApplicationSettings): "Enables the full set of features to be used for NiceUI" ), ) + DYNAMIC_SCHEDULER_UI_MOUNT_PATH: str = Field( + "/dynamic-scheduler/", + description="path on the URL where the dashboard is mounted", + ) DYNAMIC_SCHEDULER_RABBITMQ: RabbitSettings = Field( json_schema_extra={"auto_default_from_env": True}, @@ -122,3 +126,11 @@ class ApplicationSettings(_BaseApplicationSettings): json_schema_extra={"auto_default_from_env": True}, description="settings for opentelemetry tracing", ) + + @field_validator("DYNAMIC_SCHEDULER_UI_MOUNT_PATH", mode="before") + @classmethod + def _ensure_ends_with_slash(cls, v: str) -> str: + if not v.endswith("/"): + msg = f"Provided mount path: '{v}' must be '/' terminated" + raise ValueError(msg) + return v diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py b/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py index 9d131549faf..be92830ee54 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/conftest.py @@ -19,6 +19,7 @@ from settings_library.rabbit import RabbitSettings from settings_library.redis import RedisSettings from settings_library.utils_service import DEFAULT_FASTAPI_PORT +from simcore_service_dynamic_scheduler.api.frontend._utils import get_settings from simcore_service_dynamic_scheduler.core.application import create_app from tenacity import AsyncRetrying, stop_after_delay, wait_fixed @@ -92,13 +93,16 @@ async def _run_server() -> None: server_task = asyncio.create_task(_run_server()) + home_page_url = ( + f"http://{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" + ) async for attempt in AsyncRetrying( reraise=True, wait=wait_fixed(0.1), stop=stop_after_delay(2) ): with attempt: async with AsyncClient(timeout=1) as client: - result = await client.get(f"http://{server_host_port}") - assert result.status_code == status.HTTP_200_OK + response = await client.get(f"{home_page_url}") + assert response.status_code == status.HTTP_200_OK yield diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py b/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py index 1cdb66ba587..73bf844271e 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_index.py @@ -13,6 +13,7 @@ click_on_text, get_legacy_service_status, get_new_style_service_status, + take_screenshot_on_error, ) from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet from models_library.api_schemas_dynamic_scheduler.dynamic_services import ( @@ -22,6 +23,7 @@ from models_library.api_schemas_webserver.projects_nodes import NodeGet from models_library.projects_nodes_io import NodeID from playwright.async_api import Page +from simcore_service_dynamic_scheduler.api.frontend._utils import get_settings from simcore_service_dynamic_scheduler.services.service_tracker import ( set_if_status_changed_for_service, set_request_as_running, @@ -47,7 +49,9 @@ async def test_index_with_elements( get_dynamic_service_start: Callable[[NodeID], DynamicServiceStart], get_dynamic_service_stop: Callable[[NodeID], DynamicServiceStop], ): - await async_page.goto(server_host_port) + await async_page.goto( + f"{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" + ) # 1. no content await assert_contains_text(async_page, "Total tracked services:") @@ -81,7 +85,9 @@ async def test_main_page( get_dynamic_service_start: Callable[[NodeID], DynamicServiceStart], mock_stop_dynamic_service: AsyncMock, ): - await async_page.goto(server_host_port) + await async_page.goto( + f"{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" + ) # 1. no content await assert_contains_text(async_page, "Total tracked services:") @@ -118,8 +124,10 @@ async def test_main_page( mock_stop_dynamic_service.assert_not_awaited() await click_on_text(async_page, "Stop Now") - async for attempt in AsyncRetrying( - reraise=True, wait=wait_fixed(0.1), stop=stop_after_delay(3) - ): - with attempt: - mock_stop_dynamic_service.assert_awaited_once() + + async with take_screenshot_on_error(async_page): + async for attempt in AsyncRetrying( + reraise=True, wait=wait_fixed(0.1), stop=stop_after_delay(3) + ): + with attempt: + mock_stop_dynamic_service.assert_awaited_once() diff --git a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py b/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py index c37b7b0a4f1..edcccb2cab6 100644 --- a/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py +++ b/services/dynamic-scheduler/tests/unit/api_frontend/test_api_frontend_routes_service.py @@ -11,6 +11,7 @@ click_on_text, get_legacy_service_status, get_new_style_service_status, + take_screenshot_on_error, ) from models_library.api_schemas_directorv2.dynamic_services import DynamicServiceGet from models_library.api_schemas_dynamic_scheduler.dynamic_services import ( @@ -19,6 +20,7 @@ from models_library.api_schemas_webserver.projects_nodes import NodeGet from models_library.projects_nodes_io import NodeID from playwright.async_api import Page +from simcore_service_dynamic_scheduler.api.frontend._utils import get_settings from simcore_service_dynamic_scheduler.services.service_tracker import ( set_if_status_changed_for_service, set_request_as_running, @@ -47,7 +49,9 @@ async def test_service_details_no_status_present( not_initialized_app, get_dynamic_service_start(node_id) ) - await async_page.goto(server_host_port) + await async_page.goto( + f"{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" + ) # 1. one service is tracked await assert_contains_text(async_page, "Total tracked services:") @@ -65,7 +69,8 @@ async def test_service_details_renders_friendly_404( app_runner: None, async_page: Page, server_host_port: str, node_id: NodeID ): # node was not started - await async_page.goto(f"{server_host_port}/service/{node_id}:details") + url = f"http://{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}service/{node_id}:details" + await async_page.goto(f"{url}") await assert_contains_text(async_page, "Sorry could not find any details for") @@ -96,7 +101,9 @@ async def test_service_details( not_initialized_app, node_id, service_status ) - await async_page.goto(server_host_port) + await async_page.goto( + f"{server_host_port}{get_settings().DYNAMIC_SCHEDULER_UI_MOUNT_PATH}" + ) # 1. one service is tracked await assert_contains_text(async_page, "Total tracked services:") @@ -114,8 +121,9 @@ async def test_service_details( # 4. click "Remove from tracking" -> confirm await click_on_text(async_page, "Remove from tracking") await click_on_text(async_page, "Remove service") - async for attempt in AsyncRetrying( - reraise=True, wait=wait_fixed(0.1), stop=stop_after_delay(3) - ): - with attempt: - mock_remove_tracked_service.assert_awaited_once() + async with take_screenshot_on_error(async_page): + async for attempt in AsyncRetrying( + reraise=True, wait=wait_fixed(0.1), stop=stop_after_delay(3) + ): + with attempt: + mock_remove_tracked_service.assert_awaited_once()