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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from functools import cached_property
from pathlib import Path
from typing import Annotated

from pydantic import BaseModel, ConfigDict, Field
from pydantic.config import JsonDict
Expand Down Expand Up @@ -89,6 +90,11 @@ class RunningDynamicServiceDetails(ServiceDetails):
alias="service_message",
)

is_collaborative: Annotated[
bool,
Field(description="True if service allows collaboration (multi-tenant access)"),
] = False

@staticmethod
def _update_json_schema_extra(schema: JsonDict) -> None:
schema.update(
Expand Down
6 changes: 3 additions & 3 deletions packages/models-library/tests/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ def test_docker_generic_tag(image_name: str, valid: bool):
def test_simcore_service_docker_label_keys(obj_data: dict[str, Any]):
simcore_service_docker_label_keys = SimcoreContainerLabels.model_validate(obj_data)
exported_dict = simcore_service_docker_label_keys.to_simcore_runtime_docker_labels()
assert all(
isinstance(v, str) for v in exported_dict.values()
), "docker labels must be strings!"
assert all(isinstance(v, str) for v in exported_dict.values()), (
"docker labels must be strings!"
)
assert all(
key.startswith(_SIMCORE_RUNTIME_DOCKER_LABEL_PREFIX) for key in exported_dict
)
Expand Down
28 changes: 15 additions & 13 deletions services/autoscaling/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,9 +756,9 @@ async def _() -> None:
assert tasks, f"no tasks available for {found_service['Spec']['Name']}"
assert len(tasks) == 1
service_task = tasks[0]
assert (
service_task["Status"]["State"] in expected_states
), f"service {found_service['Spec']['Name']}'s task is {service_task['Status']['State']}"
assert service_task["Status"]["State"] in expected_states, (
f"service {found_service['Spec']['Name']}'s task is {service_task['Status']['State']}"
)
ctx.logger.info(
"%s",
f"service {found_service['Spec']['Name']} is now {service_task['Status']['State']} {'.' * number_of_success['count']}",
Expand Down Expand Up @@ -985,7 +985,9 @@ def _creator(
assert (
datetime.timedelta(seconds=10)
< app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_TIME_BEFORE_TERMINATION
), "this tests relies on the fact that the time before termination is above 10 seconds"
), (
"this tests relies on the fact that the time before termination is above 10 seconds"
)
assert app_settings.AUTOSCALING_EC2_INSTANCES
seconds_delta = (
-datetime.timedelta(seconds=10)
Expand Down Expand Up @@ -1126,12 +1128,12 @@ def ec2_instances_allowed_types_with_only_1_buffered(
allowed_ec2_types.items(),
)
)
assert (
allowed_ec2_types_with_buffer_defined
), "one type with buffer is needed for the tests!"
assert (
len(allowed_ec2_types_with_buffer_defined) == 1
), "more than one type with buffer is disallowed in this test!"
assert allowed_ec2_types_with_buffer_defined, (
"one type with buffer is needed for the tests!"
)
assert len(allowed_ec2_types_with_buffer_defined) == 1, (
"more than one type with buffer is disallowed in this test!"
)
return {
TypeAdapter(InstanceTypeType).validate_python(k): v
for k, v in allowed_ec2_types_with_buffer_defined.items()
Expand All @@ -1155,9 +1157,9 @@ def _by_buffer_count(
filter(_by_buffer_count, allowed_ec2_types.items())
)
assert allowed_ec2_types_with_buffer_defined, "you need one type with buffer"
assert (
len(allowed_ec2_types_with_buffer_defined) == 1
), "more than one type with buffer is disallowed in this test!"
assert len(allowed_ec2_types_with_buffer_defined) == 1, (
"more than one type with buffer is disallowed in this test!"
)
return next(iter(allowed_ec2_types_with_buffer_defined.values())).buffer_count


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,9 @@ async def _test_cluster_scaling_up_and_down( # noqa: PLR0915
all_instances = await ec2_client.describe_instances(Filters=instance_type_filters)
assert not all_instances["Reservations"]

assert (
scale_up_params.expected_num_instances == 1
), "This test is not made to work with more than 1 expected instance. so please adapt if needed"
assert scale_up_params.expected_num_instances == 1, (
"This test is not made to work with more than 1 expected instance. so please adapt if needed"
)

# create the service(s)
created_docker_services = await create_services_batch(scale_up_params)
Expand Down Expand Up @@ -1283,7 +1283,9 @@ async def test_cluster_adapts_machines_on_the_fly( # noqa: PLR0915
assert (
scale_up_params1.num_services
>= app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES
), "this test requires to run a first batch of more services than the maximum number of instances allowed"
), (
"this test requires to run a first batch of more services than the maximum number of instances allowed"
)
# we have nothing running now
all_instances = await ec2_client.describe_instances()
assert not all_instances["Reservations"]
Expand Down Expand Up @@ -1500,7 +1502,9 @@ async def test_cluster_adapts_machines_on_the_fly( # noqa: PLR0915
assert "Instances" in reservation1
assert len(reservation1["Instances"]) == (
app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES
), f"expected {app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES} EC2 instances, found {len(reservation1['Instances'])}"
), (
f"expected {app_settings.AUTOSCALING_EC2_INSTANCES.EC2_INSTANCES_MAX_INSTANCES} EC2 instances, found {len(reservation1['Instances'])}"
)
for instance in reservation1["Instances"]:
assert "InstanceType" in instance
assert instance["InstanceType"] == scale_up_params1.expected_instance_type
Expand All @@ -1514,9 +1518,9 @@ async def test_cluster_adapts_machines_on_the_fly( # noqa: PLR0915

reservation2 = all_instances["Reservations"][1]
assert "Instances" in reservation2
assert (
len(reservation2["Instances"]) == 1
), f"expected 1 EC2 instances, found {len(reservation2['Instances'])}"
assert len(reservation2["Instances"]) == 1, (
f"expected 1 EC2 instances, found {len(reservation2['Instances'])}"
)
for instance in reservation2["Instances"]:
assert "InstanceType" in instance
assert instance["InstanceType"] == scale_up_params2.expected_instance_type
Expand Down Expand Up @@ -2243,9 +2247,9 @@ async def test_warm_buffers_only_replace_hot_buffer_if_service_is_started_issue7
# BUG REPRODUCTION
#
# start a service that imposes same type as the hot buffer
assert (
hot_buffer_instance_type == "t2.xlarge"
), "the test is hard-coded for this type and accordingly resource. If this changed then the resource shall be changed too"
assert hot_buffer_instance_type == "t2.xlarge", (
"the test is hard-coded for this type and accordingly resource. If this changed then the resource shall be changed too"
)
scale_up_params = _ScaleUpParams(
imposed_instance_type=hot_buffer_instance_type,
service_resources=Resources(
Expand Down
6 changes: 6 additions & 0 deletions services/director-v2/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2878,6 +2878,12 @@
],
"title": "Service Message",
"description": "additional information related to service state"
},
"is_collaborative": {
"type": "boolean",
"title": "Is Collaborative",
"description": "True if service allows collaboration (multi-tenant access)",
"default": false
}
},
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,7 @@ async def assemble_spec( # pylint: disable=too-many-arguments # noqa: PLR0913
app.state.settings.DIRECTOR_V2_DOCKER_REGISTRY
)

docker_compose_version = (
app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SCHEDULER.DYNAMIC_SIDECAR_DOCKER_COMPOSE_VERSION
)
docker_compose_version = app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SCHEDULER.DYNAMIC_SIDECAR_DOCKER_COMPOSE_VERSION

egress_proxy_settings: EgressProxySettings = (
app.state.settings.DYNAMIC_SERVICES.DYNAMIC_SIDECAR_EGRESS_PROXY_SETTINGS
Expand Down Expand Up @@ -348,19 +346,21 @@ async def assemble_spec( # pylint: disable=too-many-arguments # noqa: PLR0913
service_version=service_version,
product_name=product_name,
)
simcore_service_labels = await resolve_and_substitute_session_variables_in_model(
app=app,
model=simcore_service_labels,
# NOTE: at this point all OsparcIdentifiers have to be replaced
# an error will be raised otherwise
safe=False,
user_id=user_id,
product_name=product_name,
product_api_base_url=product_api_base_url,
project_id=project_id,
node_id=node_id,
service_run_id=service_run_id,
wallet_id=wallet_id,
simcore_service_labels = (
await resolve_and_substitute_session_variables_in_model(
app=app,
model=simcore_service_labels,
# NOTE: at this point all OsparcIdentifiers have to be replaced
# an error will be raised otherwise
safe=False,
user_id=user_id,
product_name=product_name,
product_api_base_url=product_api_base_url,
project_id=project_id,
node_id=node_id,
service_run_id=service_run_id,
wallet_id=wallet_id,
)
)

add_egress_configuration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ def get_dynamic_proxy_spec(
The proxy is used to create network isolation
from the rest of the platform.
"""
assert (
scheduler_data.product_name is not None
), "ONLY for legacy. This function should not be called with product_name==None" # nosec
assert scheduler_data.product_name is not None, (
"ONLY for legacy. This function should not be called with product_name==None"
) # nosec

proxy_settings: DynamicSidecarProxySettings = (
dynamic_services_settings.DYNAMIC_SIDECAR_PROXY_SETTINGS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,9 @@ async def get_dynamic_sidecar_spec( # pylint:disable=too-many-arguments# noqa:
dynamic_sidecar_settings=dynamic_sidecar_settings, app_settings=app_settings
)

assert (
scheduler_data.product_name is not None
), "ONLY for legacy. This function should not be called with product_name==None" # nosec
assert scheduler_data.product_name is not None, (
"ONLY for legacy. This function should not be called with product_name==None"
) # nosec

standard_simcore_docker_labels: dict[DockerLabelKey, str] = SimcoreContainerLabels(
user_id=scheduler_data.user_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def create_model_from_scheduler_data(
"service_port": scheduler_data.service_port,
"service_state": service_state.value,
"service_message": service_message,
"is_collaborative": scheduler_data.is_collaborative,
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def test_running_service_details_make_status(
"service_host": scheduler_data.service_name,
"user_id": scheduler_data.user_id,
"service_port": scheduler_data.service_port,
"is_collaborative": scheduler_data.is_collaborative,
}

assert running_service_details_dict == expected_running_service_details
Expand Down
6 changes: 6 additions & 0 deletions services/dynamic-scheduler/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@
],
"title": "Service Message",
"description": "additional information related to service state"
},
"is_collaborative": {
"type": "boolean",
"title": "Is Collaborative",
"description": "True if service allows collaboration (multi-tenant access)",
"default": false
}
},
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16915,6 +16915,11 @@ components:
- type: 'null'
title: Service Message
description: additional information related to service state
is_collaborative:
type: boolean
title: Is Collaborative
description: True if service allows collaboration (multi-tenant access)
default: false
type: object
required:
- service_key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ async def open_project(request: web.Request) -> web.Response:

# notify users that project is now opened
project = await _projects_service.add_project_states_for_user(
user_id=req_ctx.user_id,
project=project,
app=request.app,
user_id=req_ctx.user_id, project=project, app=request.app
)
await _projects_service.notify_project_state_update(request.app, project)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
)
# Appends state
new_project = await _projects_service.add_project_states_for_user(
user_id=user_id,
project=new_project,
app=request.app,
user_id=user_id, project=new_project, app=request.app
)
await progress.update()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ async def _aggregate_data_to_projects_from_other_sources(
# udpating `project.state`
update_state_per_project = [
_projects_service.add_project_states_for_user(
user_id=user_id,
project=prj,
app=app,
user_id=user_id, project=prj, app=app
)
for prj in db_projects
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,7 @@ async def get_project_for_user(
# adds state if it is not a template
if include_state:
project = await add_project_states_for_user(
user_id=user_id,
project=project,
app=app,
user_id=user_id, project=project, app=app
)

# adds `trashed_by_primary_gid`
Expand Down Expand Up @@ -1367,22 +1365,31 @@ async def is_node_id_present_in_any_project_workbench(


async def _get_node_share_state(
app: web.Application, *, user_id: UserID, project_uuid: ProjectID, node_id: NodeID
app: web.Application,
*,
user_id: UserID,
project_uuid: ProjectID,
node_id: NodeID,
) -> NodeShareState:
node = await _projects_nodes_repository.get(
app, project_id=project_uuid, node_id=node_id
)

if _is_node_dynamic(node.key):
# if the service is dynamic and running it is locked
# if the service is dynamic and running it is locked if it is not collaborative
service = await dynamic_scheduler_service.get_dynamic_service(
app, node_id=node_id
)

if isinstance(service, DynamicServiceGet | NodeGet):
# service is running
is_collaborative_service = False
if isinstance(service, DynamicServiceGet):
# only dynamic-sidecar powered services can be collaborative
is_collaborative_service = service.is_collaborative

return NodeShareState(
locked=True,
locked=not is_collaborative_service,
current_user_groupids=[
await users_service.get_user_primary_group_id(
app, TypeAdapter(UserID).validate_python(service.user_id)
Expand Down
Loading