Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
79 changes: 79 additions & 0 deletions services/catalog/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,59 @@
}
}
},
"/v0/services/{service_key}/{service_version}/labels": {
"get": {
"tags": [
"services"
],
"summary": "Get Service Labels",
"operationId": "get_service_labels_v0_services__service_key___service_version__labels_get",
"parameters": [
{
"name": "service_key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$",
"title": "Service Key"
}
},
{
"name": "service_version",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$",
"title": "Service Version"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope_dict_str__Any__"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v0/services/{service_key}/{service_version}/specifications": {
"get": {
"tags": [
Expand Down Expand Up @@ -1470,6 +1523,32 @@
"title": "EndpointSpec",
"description": "Properties that can be configured to access and load balance a service."
},
"Envelope_dict_str__Any__": {
"properties": {
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
],
"title": "Data"
},
"error": {
"anyOf": [
{},
{
"type": "null"
}
],
"title": "Error"
}
},
"type": "object",
"title": "Envelope[dict[str, Any]]"
},
"FailureAction": {
"type": "string",
"enum": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import urllib.parse
from typing import Annotated, Any, cast

from fastapi import APIRouter, Depends
from models_library.generics import Envelope
from models_library.services import ServiceKey, ServiceVersion

from ...services.director import DirectorApi
from ..dependencies.director import get_director_api

router = APIRouter()


@router.get("/{service_key:path}/{service_version}/labels")
async def get_service_labels(
service_key: ServiceKey,
service_version: ServiceVersion,
director_client: Annotated[DirectorApi, Depends(get_director_api)],
) -> Envelope[dict[str, Any]]: # TODO: change the type
response = await director_client.get(
f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}/labels"
)
# TODO: remove the envelope since it does not make sense
return Envelope[dict[str, Any]](data=cast(dict[str, Any], response))
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
_meta,
_services,
_services_access_rights,
_services_labels,
_services_ports,
_services_resources,
_services_specifications,
Expand Down Expand Up @@ -38,6 +39,11 @@
tags=_SERVICE_TAGS,
prefix=_SERVICE_PREFIX,
)
v0_router.include_router(
_services_labels.router,
tags=_SERVICE_TAGS,
prefix=_SERVICE_PREFIX,
)
v0_router.include_router(
_services_specifications.router,
tags=_SERVICE_TAGS,
Expand Down
55 changes: 31 additions & 24 deletions services/catalog/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,41 @@ def mocked_director_service_api_base(
yield respx_mock


@pytest.fixture
def get_mocked_service_labels() -> Callable[[str, str], dict]:
def _(service_key: str, service_version: str) -> dict:
return {
"io.simcore.authors": '{"authors": [{"name": "John Smith", "email": "[email protected]", "affiliation": "ACME\'IS Foundation"}]}',
"io.simcore.contact": '{"contact": "[email protected]"}',
"io.simcore.description": '{"description": "Autonomous Nervous System Network model"}',
"io.simcore.inputs": '{"inputs": {"input_1": {"displayOrder": 1.0, "label": "Simulation time", "description": "Duration of the simulation", "type": "ref_contentSchema", "contentSchema": {"type": "number", "x_unit": "milli-second"}, "defaultValue": 2.0}}}',
"io.simcore.integration-version": '{"integration-version": "1.0.0"}',
"io.simcore.key": '{"key": "xxxxx"}'.replace("xxxxx", service_key),
"io.simcore.name": '{"name": "Autonomous Nervous System Network model"}',
"io.simcore.outputs": '{"outputs": {"output_1": {"displayOrder": 1.0, "label": "ANS output", "description": "Output of simulation of Autonomous Nervous System Network model", "type": "data:*/*", "fileToKeyMap": {"ANS_output.txt": "output_1"}}, "output_2": {"displayOrder": 2.0, "label": "Stimulation parameters", "description": "stim_param.txt file containing the input provided in the inputs port", "type": "data:*/*", "fileToKeyMap": {"ANS_stim_param.txt": "output_2"}}}}',
"io.simcore.thumbnail": '{"thumbnail": "https://www.statnews.com/wp-content/uploads/2020/05/3D-rat-heart.-iScience--768x432.png"}',
"io.simcore.type": '{"type": "computational"}',
"io.simcore.version": '{"version": "xxxxx"}'.replace(
"xxxxx", service_version
),
"maintainer": "johnsmith",
"org.label-schema.build-date": "2023-04-17T08:04:15Z",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vcs-ref": "",
"org.label-schema.vcs-url": "",
"simcore.service.restart-policy": "no-restart",
"simcore.service.settings": '[{"name": "Resources", "type": "Resources", "value": {"Limits": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}, "Reservations": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}}}]',
}

return _


@pytest.fixture
def mocked_director_service_api(
mocked_director_service_api_base: respx.MockRouter,
director_service_openapi_specs: dict[str, Any],
expected_director_list_services: list[dict[str, Any]],
get_mocked_service_labels: Callable[[str, str], dict],
) -> respx.MockRouter:
"""
STANDARD fixture to mock director service API
Expand Down Expand Up @@ -461,30 +491,7 @@ def _get_service_labels(request, service_key, service_version):
return httpx.Response(
status_code=status.HTTP_200_OK,
json={
"data": {
"io.simcore.authors": '{"authors": [{"name": "John Smith", "email": "[email protected]", "affiliation": "ACME\'IS Foundation"}]}',
"io.simcore.contact": '{"contact": "[email protected]"}',
"io.simcore.description": '{"description": "Autonomous Nervous System Network model"}',
"io.simcore.inputs": '{"inputs": {"input_1": {"displayOrder": 1.0, "label": "Simulation time", "description": "Duration of the simulation", "type": "ref_contentSchema", "contentSchema": {"type": "number", "x_unit": "milli-second"}, "defaultValue": 2.0}}}',
"io.simcore.integration-version": '{"integration-version": "1.0.0"}',
"io.simcore.key": '{"key": "xxxxx"}'.replace(
"xxxxx", found["key"]
),
"io.simcore.name": '{"name": "Autonomous Nervous System Network model"}',
"io.simcore.outputs": '{"outputs": {"output_1": {"displayOrder": 1.0, "label": "ANS output", "description": "Output of simulation of Autonomous Nervous System Network model", "type": "data:*/*", "fileToKeyMap": {"ANS_output.txt": "output_1"}}, "output_2": {"displayOrder": 2.0, "label": "Stimulation parameters", "description": "stim_param.txt file containing the input provided in the inputs port", "type": "data:*/*", "fileToKeyMap": {"ANS_stim_param.txt": "output_2"}}}}',
"io.simcore.thumbnail": '{"thumbnail": "https://www.statnews.com/wp-content/uploads/2020/05/3D-rat-heart.-iScience--768x432.png"}',
"io.simcore.type": '{"type": "computational"}',
"io.simcore.version": '{"version": "xxxxx"}'.replace(
"xxxxx", found["version"]
),
"maintainer": "iavarone",
"org.label-schema.build-date": "2023-04-17T08:04:15Z",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vcs-ref": "",
"org.label-schema.vcs-url": "",
"simcore.service.restart-policy": "no-restart",
"simcore.service.settings": '[{"name": "Resources", "type": "Resources", "value": {"Limits": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}, "Reservations": {"NanoCPUs": 4000000000, "MemoryBytes": 2147483648}}}]',
}
"data": get_mocked_service_labels(found["key"], found["version"])
},
)
return httpx.Response(
Expand Down
34 changes: 34 additions & 0 deletions services/catalog/tests/unit/test_utils_service_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# pylint: disable=redefined-outer-name
# pylint: disable=unused-argument

from collections.abc import Callable
from unittest.mock import AsyncMock

import pytest
from fastapi import FastAPI
from httpx import AsyncClient
from respx import MockRouter


@pytest.fixture
def mock_engine(app: FastAPI) -> None:
app.state.engine = AsyncMock()


async def test_get_service_labels(
postgres_setup_disabled: None,
mocked_director_service_api: MockRouter,
rabbitmq_and_rpc_setup_disabled: None,
background_tasks_setup_disabled: None,
mock_engine: None,
get_mocked_service_labels: Callable[[str, str], dict],
aclient: AsyncClient,
):
service_key = "simcore/services/comp/ans-model"
service_version = "3.0.0"
result = await aclient.get(f"/v0/services/{service_key}/{service_version}/labels")
assert result.status_code == 200, result.text
assert result.json() == {
"data": get_mocked_service_labels(service_key, service_version),
"error": None,
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async def director_client(app: FastAPI) -> DirectorApi:

# ensures manifest API cache is reset
assert hasattr(manifest.get_service, "cache")
assert manifest.get_service.cache.clear()
assert await manifest.get_service.cache.clear()

return director_api

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from models_library.projects import ProjectAtDB, ProjectID
from models_library.projects_nodes_io import NodeID
from models_library.service_settings_labels import SimcoreServiceLabels
from models_library.services import ServiceKeyVersion
from models_library.users import UserID
from pydantic import NonNegativeFloat, NonNegativeInt
from servicelib.fastapi.requests_decorators import cancel_on_disconnect
Expand All @@ -32,11 +31,13 @@
from tenacity.stop import stop_after_delay
from tenacity.wait import wait_fixed

from ...api.dependencies.catalog import get_catalog_client
from ...api.dependencies.database import get_repository
from ...api.dependencies.rabbitmq import get_rabbitmq_client_from_request
from ...core.dynamic_services_settings import DynamicServicesSettings
from ...core.dynamic_services_settings.scheduler import DynamicServicesSchedulerSettings
from ...modules import projects_networks
from ...modules.catalog import CatalogClient
from ...modules.db.repositories.projects import ProjectsRepository
from ...modules.db.repositories.projects_networks import ProjectsNetworksRepository
from ...modules.director_v0 import DirectorV0Client
Expand Down Expand Up @@ -104,6 +105,7 @@ async def list_tracked_dynamic_services(
@log_decorator(logger=logger)
async def create_dynamic_service(
service: DynamicServiceCreate,
catalog_client: Annotated[CatalogClient, Depends(get_catalog_client)],
director_v0_client: Annotated[DirectorV0Client, Depends(get_director_v0_client)],
dynamic_services_settings: Annotated[
DynamicServicesSettings, Depends(get_dynamic_services_settings)
Expand All @@ -114,9 +116,7 @@ async def create_dynamic_service(
x_simcore_user_agent: str = Header(...),
) -> DynamicServiceGet | RedirectResponse:
simcore_service_labels: SimcoreServiceLabels = (
await director_v0_client.get_service_labels(
service=ServiceKeyVersion(key=service.key, version=service.version)
)
await catalog_client.get_service_labels(service.key, service.version)
)

# LEGACY (backwards compatibility)
Expand Down Expand Up @@ -324,7 +324,7 @@ async def update_projects_networks(
ProjectsRepository, Depends(get_repository(ProjectsRepository))
],
scheduler: Annotated[DynamicSidecarsScheduler, Depends(get_scheduler)],
director_v0_client: Annotated[DirectorV0Client, Depends(get_director_v0_client)],
catalog_client: Annotated[CatalogClient, Depends(get_catalog_client)],
rabbitmq_client: Annotated[
RabbitMQClient, Depends(get_rabbitmq_client_from_request)
],
Expand All @@ -334,7 +334,7 @@ async def update_projects_networks(
projects_networks_repository=projects_networks_repository,
projects_repository=projects_repository,
scheduler=scheduler,
director_v0_client=director_v0_client,
catalog_client=catalog_client,
rabbitmq_client=rabbitmq_client,
project_id=project_id,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from rich.live import Live
from rich.table import Table
from servicelib.services_utils import get_service_from_key
from simcore_service_director_v2.modules.catalog import CatalogClient
from tenacity.asyncio import AsyncRetrying
from tenacity.stop import stop_after_attempt
from tenacity.wait import wait_random_exponential
Expand All @@ -25,7 +26,6 @@
from ..models.dynamic_services_scheduler import DynamicSidecarNamesHelper
from ..modules import db, director_v0, dynamic_sidecar
from ..modules.db.repositories.projects import ProjectsRepository
from ..modules.director_v0 import DirectorV0Client
from ..modules.dynamic_sidecar import api_client
from ..modules.projects_networks import requires_dynamic_sidecar
from ..utils.db import get_repository
Expand Down Expand Up @@ -101,7 +101,7 @@ async def async_project_save_state(project_id: ProjectID, save_attempts: int) ->
if not await requires_dynamic_sidecar(
service_key=node_content.key,
service_version=node_content.version,
director_v0_client=DirectorV0Client.instance(app),
catalog_client=CatalogClient.instance(app),
):
continue

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import httpx
from fastapi import FastAPI, HTTPException, status
from models_library.service_settings_labels import SimcoreServiceLabels
from models_library.services import ServiceKey, ServiceVersion
from models_library.services_resources import ServiceResourcesDict
from models_library.users import UserID
Expand All @@ -14,6 +15,7 @@
from settings_library.tracing import TracingSettings

from ..utils.client_decorators import handle_errors, handle_retry
from ..utils.clients import unenvelope_or_raise_error

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,7 +66,7 @@ def create(cls, app: FastAPI, **kwargs):

@classmethod
def instance(cls, app: FastAPI) -> "CatalogClient":
assert type(app.state.catalog_client) == CatalogClient # nosec
assert isinstance(app.state.catalog_client, CatalogClient) # nosec
return app.state.catalog_client

@handle_errors("Catalog", logger)
Expand Down Expand Up @@ -107,6 +109,18 @@ async def get_service_resources(
return json_response
raise HTTPException(status_code=resp.status_code, detail=resp.content)

async def get_service_labels(
self, service_key: ServiceKey, service_version: ServiceVersion
) -> SimcoreServiceLabels:
resp = await self.request(
"GET",
f"/services/{urllib.parse.quote( service_key, safe='')}/{service_version}/labels",
)
resp.raise_for_status()
if resp.status_code == status.HTTP_200_OK:
return SimcoreServiceLabels.model_validate(unenvelope_or_raise_error(resp))
raise HTTPException(status_code=resp.status_code, detail=resp.content)

async def get_service_specifications(
self, user_id: UserID, service_key: ServiceKey, service_version: ServiceVersion
) -> dict[str, Any]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ async def _get_node_infos(
] = await asyncio.gather(
_get_service_details(catalog_client, user_id, product_name, node),
director_client.get_service_extras(node.key, node.version),
director_client.get_service_labels(node),
catalog_client.get_service_labels(node.key, node.version),
)
return result

Expand Down
Loading
Loading