Skip to content

🎨 Allow project node patch of service key #6085

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

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
Expand Up @@ -26,6 +26,7 @@ class NodeCreate(InputSchemaWithoutCamelCase):


class NodePatch(InputSchemaWithoutCamelCase):
service_key: ServiceKey = FieldNotRequired(alias="key")
service_version: ServiceVersion = FieldNotRequired(alias="version")
label: str = FieldNotRequired()
inputs: InputsDict = FieldNotRequired()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
)
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
from servicelib.rabbitmq import RPCServerError
from servicelib.rabbitmq.rpc_interfaces.catalog.errors import (
CatalogForbiddenError,
CatalogItemNotFoundError,
)
from servicelib.rabbitmq.rpc_interfaces.dynamic_scheduler.errors import (
ServiceWaitingForManualInterventionError,
ServiceWasNotFoundError,
Expand Down Expand Up @@ -99,6 +103,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
UserDefaultWalletNotFoundError,
DefaultPricingUnitNotFoundError,
GroupNotFoundError,
CatalogItemNotFoundError,
) as exc:
raise web.HTTPNotFound(reason=f"{exc}") from exc
except WalletNotEnoughCreditsError as exc:
Expand All @@ -111,6 +116,8 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
raise web.HTTPServiceUnavailable(reason=f"{exc}") from exc
except ProjectNodeRequiredInputsNotSetError as exc:
raise web.HTTPConflict(reason=f"{exc}") from exc
except CatalogForbiddenError as exc:
raise web.HTTPForbidden(reason=f"{exc}") from exc

return wrapper

Expand Down Expand Up @@ -172,6 +179,7 @@ async def create_node(request: web.Request) -> web.Response:
@login_required
@permission_required("project.node.read")
@_handle_project_nodes_exceptions
# NOTE: Careful, this endpoint is actually "get_node_state," and it doesn't return a Node resource.
async def get_node(request: web.Request) -> web.Response:
req_ctx = RequestContext.parse_obj(request)
path_params = parse_request_path_parameters_as(NodePathParams, request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ async def remove_project_node(
async with self.engine.acquire() as conn:
await project_nodes_repo.delete(conn, node_id=node_id)

async def get_project_node(
async def get_project_node( # NOTE: Not all Node data are here yet; they are in the workbench of a Project, waiting to be moved here.
self, project_id: ProjectID, node_id: NodeID
) -> ProjectNode:
project_nodes_repo = ProjectNodesRepo(project_uuid=project_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
)
from servicelib.logging_utils import get_log_record_extra, log_context
from servicelib.rabbitmq import RemoteMethodNotRegisteredError, RPCServerError
from servicelib.rabbitmq.rpc_interfaces.catalog import services as catalog_rpc
from servicelib.rabbitmq.rpc_interfaces.clusters_keeper.ec2_instances import (
get_instance_type_details,
)
Expand Down Expand Up @@ -916,7 +917,27 @@ async def patch_project_node(
if not _user_project_access_rights.write:
raise ProjectInvalidRightsError(user_id=user_id, project_uuid=project_id)

# 2. Patch the project node
# 2. If patching service key or version make sure it's valid
if _node_patch_exclude_unset.get("key") or _node_patch_exclude_unset.get("version"):
_project, _ = await db.get_project(
user_id=user_id, project_uuid=f"{project_id}"
)
_project_node_data = _project["workbench"][f"{node_id}"]

_service_key = _node_patch_exclude_unset.get("key", _project_node_data["key"])
_service_version = _node_patch_exclude_unset.get(
"version", _project_node_data["version"]
)
rabbitmq_rpc_client = get_rabbitmq_rpc_client(app)
await catalog_rpc.check_for_service(
rabbitmq_rpc_client,
product_name=product_name,
user_id=user_id,
service_key=_service_key,
service_version=_service_version,
)

# 3. Patch the project node
updated_project, _ = await db.update_project_node_data(
user_id=user_id,
project_uuid=project_id,
Expand All @@ -925,7 +946,7 @@ async def patch_project_node(
new_node_data=_node_patch_exclude_unset,
)

# 3. Notify project node update
# 4. Notify project node update
await notify_project_node_update(app, updated_project, node_id, errors=None)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from pytest_simcore.helpers.assert_checks import assert_status
from pytest_simcore.helpers.webserver_login import UserInfoDict
from servicelib.aiohttp import status
from servicelib.rabbitmq.rpc_interfaces.catalog.errors import (
CatalogForbiddenError,
CatalogItemNotFoundError,
)
from simcore_service_webserver._meta import api_version_prefix
from simcore_service_webserver.db.models import UserRole
from simcore_service_webserver.projects.models import ProjectDict
Expand All @@ -40,6 +44,15 @@ def mock_project_uses_available_services(mocker: MockerFixture):
)


@pytest.fixture
def mock_catalog_rpc_check_for_service(mocker: MockerFixture):
mocker.patch(
"simcore_service_webserver.projects.projects_api.catalog_rpc.check_for_service",
spec=True,
return_value=True,
)


@pytest.mark.parametrize(
"user_role,expected",
[
Expand Down Expand Up @@ -77,8 +90,9 @@ async def test_patch_project_node(
logged_user: UserInfoDict,
user_project: ProjectDict,
expected: HTTPStatus,
mock_catalog_api_get_services_for_user_in_product,
mock_project_uses_available_services,
mock_catalog_api_get_services_for_user_in_product: None,
mock_project_uses_available_services: None,
mock_catalog_rpc_check_for_service: None,
):
node_id = next(iter(user_project["workbench"]))
assert client.app
Expand All @@ -92,6 +106,13 @@ async def test_patch_project_node(
),
)
await assert_status(resp, expected)
# service key
_patch_key = {"key": "simcore/services/dynamic/patch-service-key"}
resp = await client.patch(
f"{base_url}",
data=json.dumps(_patch_key),
)
await assert_status(resp, expected)
# service version
_patch_version = {"version": "2.0.9"}
resp = await client.patch(
Expand Down Expand Up @@ -158,6 +179,7 @@ async def test_patch_project_node(

assert _tested_node["label"] == "testing-string"
assert _tested_node["progress"] == None
assert _tested_node["key"] == _patch_key["key"]
assert _tested_node["version"] == _patch_version["version"]
assert _tested_node["inputs"] == _patch_inputs["inputs"]
assert _tested_node["inputsRequired"] == _patch_inputs_required["inputsRequired"]
Expand Down Expand Up @@ -216,3 +238,37 @@ async def test_patch_project_node_inputs_with_data_type_change(
)
await assert_status(resp, expected)
assert _patch_inputs["inputs"] == _patch_inputs["inputs"]


@pytest.mark.parametrize(
"user_role,expected", [(UserRole.USER, status.HTTP_204_NO_CONTENT)]
)
async def test_patch_project_node_service_key_with_error(
client: TestClient,
logged_user: UserInfoDict,
user_project: ProjectDict,
expected: HTTPStatus,
mock_catalog_api_get_services_for_user_in_product,
mock_project_uses_available_services,
mocker: MockerFixture,
):
node_id = next(iter(user_project["workbench"]))
assert client.app
base_url = client.app.router["patch_project_node"].url_for(
project_id=user_project["uuid"], node_id=node_id
)
_patch_version = {"version": "2.0.9"}

with mocker.patch(
"simcore_service_webserver.projects.projects_api.catalog_rpc.check_for_service",
side_effect=CatalogForbiddenError(name="test"),
):
resp = await client.patch(f"{base_url}", json=_patch_version)
assert resp.status == status.HTTP_403_FORBIDDEN

with mocker.patch(
"simcore_service_webserver.projects.projects_api.catalog_rpc.check_for_service",
side_effect=CatalogItemNotFoundError(name="test"),
):
resp = await client.patch(f"{base_url}", json=_patch_version)
assert resp.status == status.HTTP_404_NOT_FOUND
Loading