Skip to content

Commit f2f9eea

Browse files
author
Andrei Neagu
committed
moved service_extras to catalog service
1 parent 54f5c6e commit f2f9eea

File tree

10 files changed

+153
-195
lines changed

10 files changed

+153
-195
lines changed

services/catalog/src/simcore_service_catalog/api/rest/_services_extras.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ async def get_service_extras(
1616
service_version: ServiceVersion,
1717
director_client: Annotated[DirectorApi, Depends(get_director_api)],
1818
) -> dict[str, Any]:
19-
labels = await director_client.get_service_extras(service_key, service_version)
20-
return Envelope[dict[str, Any]](data=labels).model_dump(mode="json")
19+
service_extras = await director_client.get_service_extras(
20+
service_key, service_version
21+
)
22+
return Envelope[dict[str, Any]](data=service_extras).model_dump(mode="json")

services/catalog/src/simcore_service_catalog/core/application.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,7 @@ def create_app(settings: ApplicationSettings | None = None) -> FastAPI:
5050
setup_tracing(app, settings.CATALOG_TRACING, APP_NAME)
5151

5252
# STARTUP-EVENT
53-
app.add_event_handler(
54-
"startup", create_on_startup(app, tracing_settings=settings.CATALOG_TRACING)
55-
)
53+
app.add_event_handler("startup", create_on_startup(app))
5654

5755
# PLUGIN SETUP
5856
setup_function_services(app)

services/catalog/src/simcore_service_catalog/core/events.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from fastapi import FastAPI
66
from servicelib.fastapi.db_asyncpg_engine import close_db_connection, connect_to_db
77
from servicelib.logging_utils import log_context
8-
from settings_library.tracing import TracingSettings
98

109
from .._meta import APP_FINISHED_BANNER_MSG, APP_STARTED_BANNER_MSG
1110
from ..db.events import setup_default_product
@@ -27,9 +26,7 @@ def _flush_finished_banner() -> None:
2726
print(APP_FINISHED_BANNER_MSG, flush=True) # noqa: T201
2827

2928

30-
def create_on_startup(
31-
app: FastAPI, tracing_settings: TracingSettings | None
32-
) -> EventCallable:
29+
def create_on_startup(app: FastAPI) -> EventCallable:
3330
async def _() -> None:
3431
_flush_started_banner()
3532

@@ -40,7 +37,7 @@ async def _() -> None:
4037

4138
if app.state.settings.CATALOG_DIRECTOR:
4239
# setup connection to director
43-
await setup_director(app, tracing_settings=tracing_settings)
40+
await setup_director(app)
4441

4542
# FIXME: check director service is in place and ready. Hand-shake??
4643
# SEE https://github.com/ITISFoundation/osparc-simcore/issues/1728

services/catalog/src/simcore_service_catalog/core/settings.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
)
88
from models_library.basic_types import LogLevel
99
from models_library.services_resources import ResourcesDict, ResourceValue
10-
from pydantic import AliasChoices, ByteSize, Field, PositiveInt, TypeAdapter
10+
from pydantic import (
11+
AliasChoices,
12+
ByteSize,
13+
Field,
14+
NonNegativeInt,
15+
PositiveInt,
16+
TypeAdapter,
17+
)
1118
from servicelib.logging_utils_filtering import LoggerName, MessageSubstring
1219
from settings_library.application import BaseApplicationSettings
1320
from settings_library.base import BaseCustomSettings
@@ -103,3 +110,6 @@ class ApplicationSettings(BaseApplicationSettings, MixinLoggingSettings):
103110
json_schema_extra={"auto_default_from_env": True},
104111
description="settings for opentelemetry tracing",
105112
)
113+
114+
DIRECTOR_DEFAULT_MAX_MEMORY: NonNegativeInt = Field(default=0)
115+
DIRECTOR_DEFAULT_MAX_NANO_CPUS: NonNegativeInt = Field(default=0)

services/catalog/src/simcore_service_catalog/services/access_rights.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import operator
77
from collections.abc import Callable
88
from datetime import UTC, datetime
9-
from typing import Any, cast
10-
from urllib.parse import quote_plus
9+
from typing import cast
1110

1211
import arrow
1312
from fastapi import FastAPI
@@ -36,12 +35,8 @@ async def _is_old_service(app: FastAPI, service: ServiceMetaDataPublished) -> bo
3635
# NOTE: https://github.com/ITISFoundation/osparc-simcore/pull/6003#discussion_r1658200909
3736
# get service build date
3837
client = get_director_api(app)
39-
data = cast(
40-
dict[str, Any],
41-
await client.get(
42-
f"/service_extras/{quote_plus(service.key)}/{service.version}"
43-
),
44-
)
38+
39+
data = await client.get_service_extras(service.key, service.version)
4540
if not data or "build_date" not in data:
4641
return True
4742
service_build_data = arrow.get(data["build_date"]).datetime

services/catalog/src/simcore_service_catalog/services/director.py

Lines changed: 128 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import asyncio
22
import functools
3+
import json
34
import logging
45
import urllib.parse
56
from collections.abc import Awaitable, Callable
67
from contextlib import suppress
7-
from typing import Any
8+
from pprint import pformat
9+
from typing import Any, Final
810

911
import httpx
1012
from common_library.json_serialization import json_dumps
@@ -13,13 +15,13 @@
1315
from models_library.services_types import ServiceKey, ServiceVersion
1416
from servicelib.fastapi.tracing import setup_httpx_client_tracing
1517
from servicelib.logging_utils import log_context
16-
from settings_library.tracing import TracingSettings
1718
from starlette import status
1819
from tenacity.asyncio import AsyncRetrying
1920
from tenacity.before_sleep import before_sleep_log
2021
from tenacity.stop import stop_after_delay
2122
from tenacity.wait import wait_random
2223

24+
from ..core.settings import ApplicationSettings
2325
from ..exceptions.errors import DirectorUnresponsiveError
2426

2527
_logger = logging.getLogger(__name__)
@@ -98,6 +100,114 @@ async def request_wrapper(
98100
return request_wrapper
99101

100102

103+
_SERVICE_RUNTIME_SETTINGS: Final[str] = "simcore.service.settings"
104+
_ORG_LABELS_TO_SCHEMA_LABELS: Final[dict[str, str]] = {
105+
"org.label-schema.build-date": "build_date",
106+
"org.label-schema.vcs-ref": "vcs_ref",
107+
"org.label-schema.vcs-url": "vcs_url",
108+
}
109+
110+
_CONTAINER_SPEC_ENTRY_NAME = "ContainerSpec".lower()
111+
_RESOURCES_ENTRY_NAME = "Resources".lower()
112+
113+
114+
def _validate_kind(entry_to_validate: dict[str, Any], kind_name: str):
115+
for element in (
116+
entry_to_validate.get("value", {})
117+
.get("Reservations", {})
118+
.get("GenericResources", [])
119+
):
120+
if element.get("DiscreteResourceSpec", {}).get("Kind") == kind_name:
121+
return True
122+
return False
123+
124+
125+
async def _get_service_extras(
126+
director_client: "DirectorApi", image_key: str, image_tag: str
127+
) -> dict[str, Any]:
128+
# check physical node requirements
129+
# all nodes require "CPU"
130+
result: dict[str, Any] = {
131+
"node_requirements": {
132+
"CPU": director_client.default_max_nano_cpus / 1.0e09,
133+
"RAM": director_client.default_max_memory,
134+
}
135+
}
136+
137+
labels = await director_client.get_service_labels(image_key, image_tag)
138+
_logger.debug("Compiling service extras from labels %s", pformat(labels))
139+
140+
if _SERVICE_RUNTIME_SETTINGS in labels:
141+
service_settings: list[dict[str, Any]] = json.loads(
142+
labels[_SERVICE_RUNTIME_SETTINGS]
143+
)
144+
for entry in service_settings:
145+
entry_name = entry.get("name", "").lower()
146+
entry_value = entry.get("value")
147+
invalid_with_msg = None
148+
149+
if entry_name == _RESOURCES_ENTRY_NAME:
150+
if entry_value and isinstance(entry_value, dict):
151+
res_limit = entry_value.get("Limits", {})
152+
res_reservation = entry_value.get("Reservations", {})
153+
# CPU
154+
result["node_requirements"]["CPU"] = (
155+
float(res_limit.get("NanoCPUs", 0))
156+
or float(res_reservation.get("NanoCPUs", 0))
157+
or director_client.default_max_nano_cpus
158+
) / 1.0e09
159+
# RAM
160+
result["node_requirements"]["RAM"] = (
161+
res_limit.get("MemoryBytes", 0)
162+
or res_reservation.get("MemoryBytes", 0)
163+
or director_client.default_max_memory
164+
)
165+
else:
166+
invalid_with_msg = f"invalid type for resource [{entry_value}]"
167+
168+
# discrete resources (custom made ones) ---
169+
# check if the service requires GPU support
170+
if not invalid_with_msg and _validate_kind(entry, "VRAM"):
171+
172+
result["node_requirements"]["GPU"] = 1
173+
if not invalid_with_msg and _validate_kind(entry, "MPI"):
174+
result["node_requirements"]["MPI"] = 1
175+
176+
elif entry_name == _CONTAINER_SPEC_ENTRY_NAME:
177+
# NOTE: some minor validation
178+
# expects {'name': 'ContainerSpec', 'type': 'ContainerSpec', 'value': {'Command': [...]}}
179+
if (
180+
entry_value
181+
and isinstance(entry_value, dict)
182+
and "Command" in entry_value
183+
):
184+
result["container_spec"] = entry_value
185+
else:
186+
invalid_with_msg = f"invalid container_spec [{entry_value}]"
187+
188+
if invalid_with_msg:
189+
_logger.warning(
190+
"%s entry [%s] encoded in settings labels of service image %s:%s",
191+
invalid_with_msg,
192+
entry,
193+
image_key,
194+
image_tag,
195+
)
196+
197+
# get org labels
198+
result.update(
199+
{
200+
sl: labels[dl]
201+
for dl, sl in _ORG_LABELS_TO_SCHEMA_LABELS.items()
202+
if dl in labels
203+
}
204+
)
205+
206+
_logger.debug("Following service extras were compiled: %s", pformat(result))
207+
208+
return result
209+
210+
101211
class DirectorApi:
102212
"""
103213
- wrapper around thin-client to simplify director's API
@@ -108,16 +218,22 @@ class DirectorApi:
108218
SEE services/catalog/src/simcore_service_catalog/api/dependencies/director.py
109219
"""
110220

111-
def __init__(
112-
self, base_url: str, app: FastAPI, tracing_settings: TracingSettings | None
113-
):
221+
def __init__(self, base_url: str, app: FastAPI):
222+
settings: ApplicationSettings = app.state.settings
223+
224+
assert settings.CATALOG_CLIENT_REQUEST # nosec
114225
self.client = httpx.AsyncClient(
115226
base_url=base_url,
116-
timeout=app.state.settings.CATALOG_CLIENT_REQUEST.HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT,
227+
timeout=settings.CATALOG_CLIENT_REQUEST.HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT,
117228
)
118-
if tracing_settings:
229+
if settings.CATALOG_TRACING:
119230
setup_httpx_client_tracing(self.client)
120-
self.vtag = app.state.settings.CATALOG_DIRECTOR.DIRECTOR_VTAG
231+
232+
assert settings.CATALOG_DIRECTOR # nosec
233+
self.vtag = settings.CATALOG_DIRECTOR.DIRECTOR_VTAG
234+
235+
self.default_max_memory = settings.DIRECTOR_DEFAULT_MAX_MEMORY
236+
self.default_max_nano_cpus = settings.DIRECTOR_DEFAULT_MAX_NANO_CPUS
121237

122238
async def close(self):
123239
await self.client.aclose()
@@ -172,32 +288,18 @@ async def get_service_extras(
172288
service_key: ServiceKey,
173289
service_version: ServiceVersion,
174290
) -> dict[str, Any]:
175-
response = await self.get(
176-
f"/service_extras/{urllib.parse.quote_plus(service_key)}/{service_version}"
177-
)
178-
assert isinstance(response, dict) # nosec
179-
return response
291+
return await _get_service_extras(self, service_key, service_version)
180292

181293

182-
async def setup_director(
183-
app: FastAPI, tracing_settings: TracingSettings | None
184-
) -> None:
294+
async def setup_director(app: FastAPI) -> None:
185295
if settings := app.state.settings.CATALOG_DIRECTOR:
186296
with log_context(
187297
_logger, logging.DEBUG, "Setup director at %s", f"{settings.base_url=}"
188298
):
189299
async for attempt in AsyncRetrying(**_director_startup_retry_policy):
190-
client = DirectorApi(
191-
base_url=settings.base_url,
192-
app=app,
193-
tracing_settings=tracing_settings,
194-
)
300+
client = DirectorApi(base_url=settings.base_url, app=app)
195301
with attempt:
196-
client = DirectorApi(
197-
base_url=settings.base_url,
198-
app=app,
199-
tracing_settings=tracing_settings,
200-
)
302+
client = DirectorApi(base_url=settings.base_url, app=app)
201303
if not await client.is_responsive():
202304
with suppress(Exception):
203305
await client.close()

services/director/src/simcore_service_director/api/rest/_service_extras.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

services/director/src/simcore_service_director/api/rest/routes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
http_exception_as_json_response,
77
)
88

9-
from . import _health, _running_interactive_services, _service_extras, _services
9+
from . import _health, _running_interactive_services, _services
1010

1111
_V0_VTAG: Final[str] = "v0"
1212

@@ -22,7 +22,6 @@ def setup_api_routes(app: FastAPI):
2222
# include the rest under /vX
2323
api_router = APIRouter(prefix=f"/{_V0_VTAG}")
2424
api_router.include_router(_services.router, tags=["services"])
25-
api_router.include_router(_service_extras.router, tags=["services"])
2625
api_router.include_router(_running_interactive_services.router, tags=["services"])
2726
app.include_router(api_router)
2827

0 commit comments

Comments
 (0)