diff --git a/api/specs/webserver/openapi-projects.yaml b/api/specs/webserver/openapi-projects.yaml index b54b5b8c8ba..c5abca85b7a 100644 --- a/api/specs/webserver/openapi-projects.yaml +++ b/api/specs/webserver/openapi-projects.yaml @@ -177,6 +177,12 @@ paths: required: true schema: type: string + - name: disable_service_auto_start + in: query + required: false + schema: + type: boolean + default: false post: tags: - project diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index d7808c42101..b8f84842b6e 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -4354,6 +4354,12 @@ paths: required: true schema: type: string + - name: disable_service_auto_start + in: query + required: false + schema: + type: boolean + default: false post: tags: - project diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_api.py b/services/web/server/src/simcore_service_webserver/projects/projects_api.py index e6d86797460..4346fe8062c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_api.py @@ -933,7 +933,10 @@ async def set_project_node_resources( async def run_project_dynamic_services( - request: web.Request, project: dict, user_id: UserID, product_name: str + request: web.Request, + project: dict, + user_id: UserID, + product_name: str, ) -> None: # first get the services if they already exist project_settings = get_settings(request.app).WEBSERVER_PROJECTS diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py b/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py index d73bccf5fae..aeb3409e6cf 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py @@ -10,7 +10,11 @@ from aiohttp import web from models_library.projects_state import ProjectState -from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as +from pydantic import BaseModel +from servicelib.aiohttp.requests_validation import ( + parse_request_path_parameters_as, + parse_request_query_parameters_as, +) from servicelib.aiohttp.web_exceptions_extension import HTTPLocked from servicelib.common_headers import ( UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE, @@ -42,12 +46,17 @@ routes = web.RouteTableDef() +class _OpenProjectQuery(BaseModel): + disable_service_auto_start: bool = False + + @routes.post(f"/{VTAG}/projects/{{project_id}}:open", name="open_project") @login_required @permission_required("project.open") async def open_project(request: web.Request) -> web.Response: req_ctx = RequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) + query_params = parse_request_query_parameters_as(_OpenProjectQuery, request) try: client_session_id = await request.json() @@ -93,13 +102,14 @@ async def open_project(request: web.Request) -> web.Response: ) # user id opened project uuid - with contextlib.suppress(ProjectStartsTooManyDynamicNodes): - # NOTE: this method raises that exception when the number of dynamic - # services in the project is highter than the maximum allowed per project - # the project shall still open though. - await projects_api.run_project_dynamic_services( - request, project, req_ctx.user_id, req_ctx.product_name - ) + if not query_params.disable_service_auto_start: + with contextlib.suppress(ProjectStartsTooManyDynamicNodes): + # NOTE: this method raises that exception when the number of dynamic + # services in the project is highter than the maximum allowed per project + # the project shall still open though. + await projects_api.run_project_dynamic_services( + request, project, req_ctx.user_id, req_ctx.product_name + ) # and let's update the project last change timestamp await projects_api.update_project_last_change_timestamp( diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_handlers__open_close.py b/services/web/server/tests/unit/with_dbs/02/test_projects_handlers__open_close.py index bce78057bde..21c2d88f55f 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_handlers__open_close.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_handlers__open_close.py @@ -494,6 +494,43 @@ async def test_open_project_with_small_amount_of_dynamic_services_starts_them_au mocked_director_v2_api["director_v2_api.run_dynamic_service"].reset_mock() +@pytest.mark.parametrize(*standard_user_role()) +async def test_open_project_with_disable_service_auto_start_set_overrides_behavior( + client: TestClient, + logged_user: UserInfoDict, + user_project_with_num_dynamic_services: Callable[[int], Awaitable[ProjectDict]], + client_session_id_factory: Callable, + expected: ExpectedResponse, + mocked_director_v2_api: dict[str, mock.Mock], + mock_catalog_api: dict[str, mock.Mock], + max_amount_of_auto_started_dyn_services: int, + faker: Faker, +): + assert client.app + num_of_dyn_services = max_amount_of_auto_started_dyn_services or faker.pyint( + min_value=3, max_value=250 + ) + project = await user_project_with_num_dynamic_services(num_of_dyn_services) + all_service_uuids = list(project["workbench"]) + for num_service_already_running in range(num_of_dyn_services): + mocked_director_v2_api["director_v2_api.list_dynamic_services"].return_value = [ + {"service_uuid": all_service_uuids[service_id]} + for service_id in range(num_service_already_running) + ] + + url = ( + client.app.router["open_project"] + .url_for(project_id=project["uuid"]) + .with_query(disable_service_auto_start=f"{True}") + ) + + resp = await client.post(f"{url}", json=client_session_id_factory()) + await assert_status(resp, expected.ok) + mocked_director_v2_api[ + "director_v2_api.run_dynamic_service" + ].assert_not_called() + + @pytest.mark.parametrize(*standard_user_role()) async def test_open_project_with_large_amount_of_dynamic_services_does_not_start_them_automatically( client: TestClient, @@ -810,7 +847,6 @@ async def test_project_node_lifetime( mocker, faker: Faker, ): - mock_storage_api_delete_data_folders_of_project_node = mocker.patch( "simcore_service_webserver.projects.projects_handlers_crud.projects_api.storage_api.delete_data_folders_of_project_node", return_value="", @@ -1181,7 +1217,6 @@ async def test_open_shared_project_at_same_time( ] # create other clients for i in range(NUMBER_OF_ADDITIONAL_CLIENTS): - new_client = client_on_running_server_factory() user = await log_client_in( new_client,