Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨Webserver: Allow to disable auto-start of dynamic services #4103

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
6 changes: 6 additions & 0 deletions api/specs/webserver/openapi-projects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
sanderegg marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand Down Expand Up @@ -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="",
Expand Down Expand Up @@ -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,
Expand Down