Skip to content

Commit 62590f3

Browse files
authored
✨ Prevent deprecated services to start (ITISFoundation#3350)
1 parent 3544897 commit 62590f3

File tree

41 files changed

+1043
-224
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1043
-224
lines changed

packages/models-library/src/models_library/services.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,10 @@ class ServiceMetaData(_BaseServiceCommonDataModel):
566566
name: Optional[str]
567567
thumbnail: Optional[HttpUrl]
568568
description: Optional[str]
569-
deprecated: Optional[datetime]
569+
deprecated: Optional[datetime] = Field(
570+
default=None,
571+
description="If filled with a date, then the service is to be deprecated at that date (e.g. cannot start anymore)",
572+
)
570573

571574
# user-defined metatada
572575
classifiers: Optional[list[str]]

packages/service-library/src/servicelib/aiohttp/requests_validation.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
from contextlib import contextmanager
7-
from typing import Iterator, Type, TypeVar
7+
from typing import Iterator, TypeVar
88

99
from aiohttp import web
1010
from pydantic import BaseModel, ValidationError
@@ -52,7 +52,12 @@ def handle_validation_as_http_error(
5252
for e in details
5353
]
5454
error_str = json_dumps(
55-
{"error": {"status": web.HTTPBadRequest.status_code, "errors": errors}}
55+
{
56+
"error": {
57+
"status": web.HTTPUnprocessableEntity.status_code,
58+
"errors": errors,
59+
}
60+
}
5661
)
5762
else:
5863
# NEW proposed error for https://github.com/ITISFoundation/osparc-simcore/issues/443
@@ -66,7 +71,7 @@ def handle_validation_as_http_error(
6671
}
6772
)
6873

69-
raise web.HTTPBadRequest(
74+
raise web.HTTPUnprocessableEntity(
7075
reason=reason_msg,
7176
text=error_str,
7277
content_type=MIMETYPE_APPLICATION_JSON,
@@ -82,7 +87,7 @@ def handle_validation_as_http_error(
8287

8388

8489
def parse_request_path_parameters_as(
85-
parameters_schema: Type[ModelType],
90+
parameters_schema: type[ModelType],
8691
request: web.Request,
8792
*,
8893
use_enveloped_error_v1: bool = True,
@@ -101,7 +106,7 @@ def parse_request_path_parameters_as(
101106

102107

103108
def parse_request_query_parameters_as(
104-
parameters_schema: Type[ModelType],
109+
parameters_schema: type[ModelType],
105110
request: web.Request,
106111
*,
107112
use_enveloped_error_v1: bool = True,
@@ -121,7 +126,7 @@ def parse_request_query_parameters_as(
121126

122127

123128
async def parse_request_body_as(
124-
model_schema: Type[ModelType],
129+
model_schema: type[ModelType],
125130
request: web.Request,
126131
*,
127132
use_enveloped_error_v1: bool = True,

packages/service-library/tests/aiohttp/test_requests_validation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ async def test_parse_request_with_invalid_path_params(
192192
params=query_params.as_params(),
193193
json=body.dict(),
194194
)
195-
assert r.status == web.HTTPBadRequest.status_code, f"{await r.text()}"
195+
assert r.status == web.HTTPUnprocessableEntity.status_code, f"{await r.text()}"
196196

197197
errors = await r.json()
198198
assert errors["error"].pop("resource")
@@ -221,7 +221,7 @@ async def test_parse_request_with_invalid_query_params(
221221
params={},
222222
json=body.dict(),
223223
)
224-
assert r.status == web.HTTPBadRequest.status_code, f"{await r.text()}"
224+
assert r.status == web.HTTPUnprocessableEntity.status_code, f"{await r.text()}"
225225

226226
errors = await r.json()
227227
assert errors["error"].pop("resource")
@@ -250,7 +250,7 @@ async def test_parse_request_with_invalid_body(
250250
params=query_params.as_params(),
251251
json={"invalid": "body"},
252252
)
253-
assert r.status == web.HTTPBadRequest.status_code, f"{await r.text()}"
253+
assert r.status == web.HTTPUnprocessableEntity.status_code, f"{await r.text()}"
254254

255255
errors = await r.json()
256256

services/api-server/src/simcore_service_api_server/api/dependencies/application.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
from fastapi import Request
44

5+
from ...core.settings import ApplicationSettings
6+
57

68
def get_reverse_url_mapper(request: Request) -> Callable:
79
def reverse_url_mapper(name: str, **path_params: Any) -> str:
810
return request.url_for(name, **path_params)
911

1012
return reverse_url_mapper
13+
14+
15+
def get_settings(request: Request) -> ApplicationSettings:
16+
return request.app.state.settings

services/api-server/src/simcore_service_api_server/api/routes/solvers.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
from pydantic import ValidationError
88
from pydantic.errors import PydanticValueError
99

10+
from ...core.settings import ApplicationSettings
1011
from ...models.schemas.solvers import Solver, SolverKeyId, VersionStr
1112
from ...modules.catalog import CatalogApi
12-
from ..dependencies.application import get_reverse_url_mapper
13+
from ..dependencies.application import get_reverse_url_mapper, get_settings
1314
from ..dependencies.authentication import get_current_user_id
1415
from ..dependencies.services import get_api_client
1516

@@ -33,9 +34,12 @@ async def list_solvers(
3334
user_id: int = Depends(get_current_user_id),
3435
catalog_client: CatalogApi = Depends(get_api_client(CatalogApi)),
3536
url_for: Callable = Depends(get_reverse_url_mapper),
37+
app_settings: ApplicationSettings = Depends(get_settings),
3638
):
3739
"""Lists all available solvers (latest version)"""
38-
solvers: list[Solver] = await catalog_client.list_latest_releases(user_id)
40+
solvers: list[Solver] = await catalog_client.list_latest_releases(
41+
user_id, product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME
42+
)
3943

4044
for solver in solvers:
4145
solver.url = url_for(
@@ -50,11 +54,14 @@ async def list_solvers_releases(
5054
user_id: int = Depends(get_current_user_id),
5155
catalog_client: CatalogApi = Depends(get_api_client(CatalogApi)),
5256
url_for: Callable = Depends(get_reverse_url_mapper),
57+
app_settings: ApplicationSettings = Depends(get_settings),
5358
):
5459
"""Lists all released solvers (all released versions)"""
5560
assert await catalog_client.is_responsive() # nosec
5661

57-
solvers: list[Solver] = await catalog_client.list_solvers(user_id)
62+
solvers: list[Solver] = await catalog_client.list_solvers(
63+
user_id, product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME
64+
)
5865

5966
for solver in solvers:
6067
solver.url = url_for(
@@ -74,13 +81,18 @@ async def get_solver(
7481
user_id: int = Depends(get_current_user_id),
7582
catalog_client: CatalogApi = Depends(get_api_client(CatalogApi)),
7683
url_for: Callable = Depends(get_reverse_url_mapper),
84+
app_settings: ApplicationSettings = Depends(get_settings),
7785
) -> Solver:
7886
"""Gets latest release of a solver"""
7987
# IMPORTANT: by adding /latest, we avoid changing the order of this entry in the router list
8088
# otherwise, {solver_key:path} will override and consume any of the paths that follow.
8189
try:
8290

83-
solver = await catalog_client.get_latest_release(user_id, solver_key)
91+
solver = await catalog_client.get_latest_release(
92+
user_id,
93+
solver_key,
94+
product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME,
95+
)
8496
solver.url = url_for(
8597
"get_solver_release", solver_key=solver.id, version=solver.version
8698
)
@@ -101,10 +113,11 @@ async def list_solver_releases(
101113
user_id: int = Depends(get_current_user_id),
102114
catalog_client: CatalogApi = Depends(get_api_client(CatalogApi)),
103115
url_for: Callable = Depends(get_reverse_url_mapper),
116+
app_settings: ApplicationSettings = Depends(get_settings),
104117
):
105118
"""Lists all releases of a given solver"""
106119
releases: list[Solver] = await catalog_client.list_solver_releases(
107-
user_id, solver_key
120+
user_id, solver_key, product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME
108121
)
109122

110123
for solver in releases:
@@ -122,10 +135,16 @@ async def get_solver_release(
122135
user_id: int = Depends(get_current_user_id),
123136
catalog_client: CatalogApi = Depends(get_api_client(CatalogApi)),
124137
url_for: Callable = Depends(get_reverse_url_mapper),
138+
app_settings: ApplicationSettings = Depends(get_settings),
125139
) -> Solver:
126140
"""Gets a specific release of a solver"""
127141
try:
128-
solver = await catalog_client.get_solver(user_id, solver_key, version)
142+
solver = await catalog_client.get_solver(
143+
user_id,
144+
solver_key,
145+
version,
146+
product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME,
147+
)
129148

130149
solver.url = url_for(
131150
"get_solver_release", solver_key=solver.id, version=solver.version

services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from models_library.projects_nodes_io import BaseFileLink
1313
from pydantic.types import PositiveInt
1414

15+
from ...core.settings import ApplicationSettings
1516
from ...models.domain.projects import NewProjectIn, Project
1617
from ...models.schemas.files import File
1718
from ...models.schemas.jobs import ArgumentType, Job, JobInputs, JobOutputs, JobStatus
@@ -30,7 +31,7 @@
3031
create_new_project_for_job,
3132
)
3233
from ...utils.solver_job_outputs import get_solver_output_results
33-
from ..dependencies.application import get_reverse_url_mapper
34+
from ..dependencies.application import get_reverse_url_mapper, get_settings
3435
from ..dependencies.authentication import get_current_user_id
3536
from ..dependencies.database import Engine, get_db_engine
3637
from ..dependencies.services import get_api_client
@@ -66,10 +67,16 @@ async def list_jobs(
6667
catalog_client: CatalogApi = Depends(get_api_client(CatalogApi)),
6768
webserver_api: AuthSession = Depends(get_webserver_session),
6869
url_for: Callable = Depends(get_reverse_url_mapper),
70+
app_settings: ApplicationSettings = Depends(get_settings),
6971
):
7072
"""List of all jobs in a specific released solver"""
7173

72-
solver = await catalog_client.get_solver(user_id, solver_key, version)
74+
solver = await catalog_client.get_solver(
75+
user_id,
76+
solver_key,
77+
version,
78+
product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME,
79+
)
7380
logger.debug("Listing Jobs in Solver '%s'", solver.name)
7481

7582
projects: list[Project] = await webserver_api.list_projects(solver.name)
@@ -97,14 +104,20 @@ async def create_job(
97104
webserver_api: AuthSession = Depends(get_webserver_session),
98105
director2_api: DirectorV2Api = Depends(get_api_client(DirectorV2Api)),
99106
url_for: Callable = Depends(get_reverse_url_mapper),
107+
app_settings: ApplicationSettings = Depends(get_settings),
100108
):
101109
"""Creates a job in a specific release with given inputs.
102110
103111
NOTE: This operation does **not** start the job
104112
"""
105113

106114
# ensures user has access to solver
107-
solver = await catalog_client.get_solver(user_id, solver_key, version)
115+
solver = await catalog_client.get_solver(
116+
user_id,
117+
solver_key,
118+
version,
119+
product_name=app_settings.API_SERVER_DEFAULT_PRODUCT_NAME,
120+
)
108121

109122
# creates NEW job as prototype
110123
pre_job = Job.create_solver_job(solver=solver, inputs=inputs)
@@ -173,12 +186,15 @@ async def start_job(
173186
job_id: UUID,
174187
user_id: PositiveInt = Depends(get_current_user_id),
175188
director2_api: DirectorV2Api = Depends(get_api_client(DirectorV2Api)),
189+
app_settings: ApplicationSettings = Depends(get_settings),
176190
):
177191

178192
job_name = _compose_job_resource_name(solver_key, version, job_id)
179193
logger.debug("Start Job '%s'", job_name)
180194

181-
task = await director2_api.start_computation(job_id, user_id)
195+
task = await director2_api.start_computation(
196+
job_id, user_id, app_settings.API_SERVER_DEFAULT_PRODUCT_NAME
197+
)
182198
job_status: JobStatus = create_jobstatus_from_task(task)
183199
return job_status
184200

services/api-server/src/simcore_service_api_server/core/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings):
9595
API_SERVER_DIRECTOR_V2: Optional[DirectorV2Settings] = Field(
9696
auto_default_from_env=True
9797
)
98+
API_SERVER_DEFAULT_PRODUCT_NAME: str = Field(
99+
default="osparc", description="The API-server default product name"
100+
)
98101

99102
# DIAGNOSTICS
100103
API_SERVER_TRACING: Optional[TracingSettings] = Field(auto_default_from_env=True)

services/api-server/src/simcore_service_api_server/modules/catalog.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import urllib.parse
33
from dataclasses import dataclass
44
from operator import attrgetter
5-
from typing import Callable, Optional, Tuple
5+
from typing import Callable, Optional
66

77
from fastapi import FastAPI
88
from models_library.services import ServiceDockerData, ServiceType
@@ -17,7 +17,7 @@
1717
logger = logging.getLogger(__name__)
1818

1919

20-
SolverNameVersionPair = Tuple[SolverKeyId, str]
20+
SolverNameVersionPair = tuple[SolverKeyId, str]
2121

2222

2323
class TruncatedCatalogServiceOut(ServiceDockerData):
@@ -78,12 +78,14 @@ class CatalogApi(BaseServiceClientApi):
7878
async def list_solvers(
7979
self,
8080
user_id: int,
81+
*,
82+
product_name: str,
8183
predicate: Optional[Callable[[Solver], bool]] = None,
8284
) -> list[Solver]:
8385
resp = await self.client.get(
8486
"/services",
8587
params={"user_id": user_id, "details": True},
86-
headers={"x-simcore-products-name": "osparc"},
88+
headers={"x-simcore-products-name": product_name},
8789
)
8890
resp.raise_for_status()
8991

@@ -109,7 +111,7 @@ async def list_solvers(
109111
return solvers
110112

111113
async def get_solver(
112-
self, user_id: int, name: SolverKeyId, version: VersionStr
114+
self, user_id: int, name: SolverKeyId, version: VersionStr, *, product_name: str
113115
) -> Solver:
114116

115117
assert version != LATEST_VERSION # nosec
@@ -120,7 +122,7 @@ async def get_solver(
120122
resp = await self.client.get(
121123
f"/services/{service_key}/{service_version}",
122124
params={"user_id": user_id},
123-
headers={"x-simcore-products-name": "osparc"},
125+
headers={"x-simcore-products-name": product_name},
124126
)
125127
resp.raise_for_status()
126128

@@ -131,8 +133,12 @@ async def get_solver(
131133

132134
return service.to_solver()
133135

134-
async def list_latest_releases(self, user_id: int) -> list[Solver]:
135-
solvers: list[Solver] = await self.list_solvers(user_id)
136+
async def list_latest_releases(
137+
self, user_id: int, *, product_name: str
138+
) -> list[Solver]:
139+
solvers: list[Solver] = await self.list_solvers(
140+
user_id, product_name=product_name
141+
)
136142

137143
latest_releases = {}
138144
for solver in solvers:
@@ -143,16 +149,22 @@ async def list_latest_releases(self, user_id: int) -> list[Solver]:
143149
return list(latest_releases.values())
144150

145151
async def list_solver_releases(
146-
self, user_id: int, solver_key: SolverKeyId
152+
self, user_id: int, solver_key: SolverKeyId, *, product_name: str
147153
) -> list[Solver]:
148154
def _this_solver(solver: Solver) -> bool:
149155
return solver.id == solver_key
150156

151-
releases: list[Solver] = await self.list_solvers(user_id, _this_solver)
157+
releases: list[Solver] = await self.list_solvers(
158+
user_id, predicate=_this_solver, product_name=product_name
159+
)
152160
return releases
153161

154-
async def get_latest_release(self, user_id: int, solver_key: SolverKeyId) -> Solver:
155-
releases = await self.list_solver_releases(user_id, solver_key)
162+
async def get_latest_release(
163+
self, user_id: int, solver_key: SolverKeyId, *, product_name: str
164+
) -> Solver:
165+
releases = await self.list_solver_releases(
166+
user_id, solver_key, product_name=product_name
167+
)
156168

157169
# raises IndexError if None
158170
latest = sorted(releases, key=attrgetter("pep404_version"))[-1]

0 commit comments

Comments
 (0)