Skip to content

Commit f407644

Browse files
authored
✨Computational clusters: connect autoscaling to RabbitMQ (#7485)
1 parent 5e1323e commit f407644

File tree

13 files changed

+190
-68
lines changed

13 files changed

+190
-68
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This document provides guidelines and best practices for using GitHub Copilot in
2121
- ensure we use `sqlalchemy` >2 compatible code.
2222
- ensure we use `pydantic` >2 compatible code.
2323
- ensure we use `fastapi` >0.100 compatible code
24-
24+
- use f-string formatting
2525

2626
## Node.js-Specific Instructions
2727

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

Lines changed: 50 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Literal, TypeAlias
44

55
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, HttpUrl, field_validator
6+
from pydantic.config import JsonDict
67
from pydantic.types import NonNegativeInt
78

89
from .groups import GroupID
@@ -36,18 +37,22 @@ class TLSAuthentication(_AuthenticationBase):
3637
tls_client_cert: Path
3738
tls_client_key: Path
3839

39-
model_config = ConfigDict(
40-
json_schema_extra={
41-
"examples": [
42-
{
43-
"type": "tls",
44-
"tls_ca_file": "/path/to/ca_file",
45-
"tls_client_cert": "/path/to/cert_file",
46-
"tls_client_key": "/path/to/key_file",
47-
},
48-
]
49-
}
50-
)
40+
@staticmethod
41+
def _update_json_schema_extra(schema: JsonDict) -> None:
42+
schema.update(
43+
{
44+
"examples": [
45+
{
46+
"type": "tls",
47+
"tls_ca_file": "/path/to/ca_file",
48+
"tls_client_cert": "/path/to/cert_file",
49+
"tls_client_key": "/path/to/key_file",
50+
},
51+
]
52+
}
53+
)
54+
55+
model_config = ConfigDict(json_schema_extra=_update_json_schema_extra)
5156

5257

5358
ClusterAuthentication: TypeAlias = NoAuthentication | TLSAuthentication
@@ -71,36 +76,41 @@ class BaseCluster(BaseModel):
7176
create_enums_pre_validator(ClusterTypeInModel)
7277
)
7378

74-
model_config = ConfigDict(
75-
use_enum_values=True,
76-
json_schema_extra={
77-
"examples": [
78-
{
79-
"name": "My awesome cluster",
80-
"type": ClusterTypeInModel.ON_PREMISE,
81-
"owner": 12,
82-
"endpoint": "https://registry.osparc-development.fake.dev",
83-
"authentication": {
84-
"type": "tls",
85-
"tls_ca_file": "/path/to/ca_file",
86-
"tls_client_cert": "/path/to/cert_file",
87-
"tls_client_key": "/path/to/key_file",
79+
@staticmethod
80+
def _update_json_schema_extra(schema: JsonDict) -> None:
81+
schema.update(
82+
{
83+
"examples": [
84+
{
85+
"name": "My awesome cluster",
86+
"type": ClusterTypeInModel.ON_PREMISE,
87+
"owner": 12,
88+
"endpoint": "https://registry.osparc-development.fake.dev",
89+
"authentication": {
90+
"type": "tls",
91+
"tls_ca_file": "/path/to/ca_file",
92+
"tls_client_cert": "/path/to/cert_file",
93+
"tls_client_key": "/path/to/key_file",
94+
},
8895
},
89-
},
90-
{
91-
"name": "My AWS cluster",
92-
"type": ClusterTypeInModel.AWS,
93-
"owner": 154,
94-
"endpoint": "https://registry.osparc-development.fake.dev",
95-
"authentication": {
96-
"type": "tls",
97-
"tls_ca_file": "/path/to/ca_file",
98-
"tls_client_cert": "/path/to/cert_file",
99-
"tls_client_key": "/path/to/key_file",
96+
{
97+
"name": "My AWS cluster",
98+
"type": ClusterTypeInModel.AWS,
99+
"owner": 154,
100+
"endpoint": "https://registry.osparc-development.fake.dev",
101+
"authentication": {
102+
"type": "tls",
103+
"tls_ca_file": "/path/to/ca_file",
104+
"tls_client_cert": "/path/to/cert_file",
105+
"tls_client_key": "/path/to/key_file",
106+
},
100107
},
101-
},
102-
]
103-
},
108+
]
109+
}
110+
)
111+
112+
model_config = ConfigDict(
113+
use_enum_values=True, json_schema_extra=_update_json_schema_extra
104114
)
105115

106116

packages/pytest-simcore/src/pytest_simcore/helpers/monkeypatch_envs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ def setenvs_from_dict(
3030
if isinstance(value, bool):
3131
v = "true" if value else "false"
3232

33+
if isinstance(value, int | float):
34+
v = f"{value}"
35+
3336
assert isinstance(v, str), (
3437
"caller MUST explicitly stringify values since some cannot be done automatically"
3538
f"e.g. json-like values. Check {key=},{value=}"

packages/service-library/tests/fastapi/test_rabbitmq_lifespan.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def mock_rabbitmq_connection(mocker: MockerFixture) -> MockType:
3939
def mock_rabbitmq_rpc_client_class(mocker: MockerFixture) -> MockType:
4040
mock_rpc_client_instance = mocker.AsyncMock()
4141
mocker.patch.object(
42-
servicelib.rabbitmq._client_rpc.RabbitMQRPCClient,
42+
servicelib.rabbitmq._client_rpc.RabbitMQRPCClient, # noqa: SLF001
4343
"create",
4444
return_value=mock_rpc_client_instance,
4545
)
@@ -87,7 +87,6 @@ async def my_app_rpc_server(app: FastAPI, state: State) -> AsyncIterator[State]:
8787

8888
# setup rpc-client using rabbitmq_rpc_client_context
8989
async def my_app_rpc_client(app: FastAPI, state: State) -> AsyncIterator[State]:
90-
9190
assert "RABBIT_CONNECTIVITY_LIFESPAN_NAME" in state
9291

9392
async with rabbitmq_rpc_client_context(
@@ -122,7 +121,6 @@ async def test_lifespan_rabbitmq_in_an_app(
122121
startup_timeout=None if is_pdb_enabled else 10,
123122
shutdown_timeout=None if is_pdb_enabled else 10,
124123
):
125-
126124
# Verify that RabbitMQ responsiveness was checked
127125
mock_rabbitmq_connection.assert_called_once_with(
128126
app.state.settings.RABBITMQ.dsn

packages/settings-library/src/settings_library/rabbit.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from functools import cached_property
2+
from typing import ClassVar
23

4+
from pydantic.config import JsonDict
35
from pydantic.networks import AnyUrl
46
from pydantic.types import SecretStr
57
from pydantic_settings import SettingsConfigDict
@@ -9,7 +11,7 @@
911

1012

1113
class RabbitDsn(AnyUrl):
12-
allowed_schemes = {"amqp", "amqps"}
14+
allowed_schemes: ClassVar[set[str]] = {"amqp", "amqps"}
1315

1416

1517
class RabbitSettings(BaseCustomSettings):
@@ -35,16 +37,31 @@ def dsn(self) -> str:
3537
)
3638
return rabbit_dsn
3739

40+
@staticmethod
41+
def _update_json_schema_extra(schema: JsonDict) -> None:
42+
schema.update(
43+
{
44+
"examples": [
45+
{
46+
"RABBIT_HOST": "rabbitmq.example.com",
47+
"RABBIT_USER": "guest",
48+
"RABBIT_PASSWORD": "guest-password",
49+
"RABBIT_SECURE": False,
50+
"RABBIT_PORT": 5672,
51+
},
52+
{
53+
"RABBIT_HOST": "secure.rabbitmq.example.com",
54+
"RABBIT_USER": "guest",
55+
"RABBIT_PASSWORD": "guest-password",
56+
"RABBIT_SECURE": True,
57+
"RABBIT_PORT": 15672,
58+
},
59+
]
60+
}
61+
)
62+
3863
model_config = SettingsConfigDict(
39-
json_schema_extra={
40-
"examples": [
41-
# minimal required
42-
{
43-
"RABBIT_SECURE": "1",
44-
"RABBIT_HOST": "localhost",
45-
"RABBIT_USER": "user",
46-
"RABBIT_PASSWORD": "foobar", # NOSONAR
47-
}
48-
],
49-
}
64+
extra="ignore",
65+
populate_by_name=True,
66+
json_schema_extra=_update_json_schema_extra,
5067
)

services/autoscaling/tests/unit/test_modules_dask.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
_authentication_types = [
4444
NoAuthentication(),
4545
TLSAuthentication.model_construct(
46-
**TLSAuthentication.model_config["json_schema_extra"]["examples"][0]
46+
**TLSAuthentication.model_json_schema()["examples"][0]
4747
),
4848
]
4949

@@ -267,7 +267,9 @@ def _add_fct(x: int, y: int) -> int:
267267
)
268268
assert isinstance(exc, RuntimeError)
269269
else:
270-
result = await future_queued_task.result(timeout=_DASK_SCHEDULER_REACTION_TIME_S) # type: ignore
270+
result = await future_queued_task.result(
271+
timeout=_DASK_SCHEDULER_REACTION_TIME_S
272+
) # type: ignore
271273
assert result == 7
272274

273275
await _wait_for_dask_scheduler_to_change_state()

services/clusters-keeper/src/simcore_service_clusters_keeper/core/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,14 @@ class PrimaryEC2InstancesSettings(BaseCustomSettings):
252252
),
253253
] = "172.20.0.0/14" # nosec
254254

255+
PRIMARY_EC2_INSTANCES_RABBIT: Annotated[
256+
RabbitSettings | None,
257+
Field(
258+
description="defines the Rabbit settings for the primary instance (may be disabled)",
259+
json_schema_extra={"auto_default_from_env": True},
260+
),
261+
]
262+
255263
@field_validator("PRIMARY_EC2_INSTANCES_ALLOWED_TYPES")
256264
@classmethod
257265
def _check_valid_instance_names(

services/clusters-keeper/src/simcore_service_clusters_keeper/data/docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ services:
9191
AUTOSCALING_EC2_SECRET_ACCESS_KEY: ${CLUSTERS_KEEPER_EC2_SECRET_ACCESS_KEY}
9292
AUTOSCALING_NODES_MONITORING: null
9393
AUTOSCALING_POLL_INTERVAL: 00:00:10
94+
AUTOSCALING_RABBITMQ: ${AUTOSCALING_RABBITMQ}
9495
DASK_MONITORING_URL: tls://dask-scheduler:8786
9596
DASK_SCHEDULER_AUTH: '{"type":"tls","tls_ca_file":"${DASK_TLS_CA_FILE}","tls_client_cert":"${DASK_TLS_CERT}","tls_client_key":"${DASK_TLS_KEY}"}'
9697
EC2_INSTANCES_ALLOWED_TYPES: ${WORKERS_EC2_INSTANCES_ALLOWED_TYPES}
@@ -188,6 +189,7 @@ services:
188189
networks:
189190
cluster:
190191

192+
191193
configs:
192194
prometheus-config:
193195
file: ./prometheus.yml
@@ -200,6 +202,7 @@ volumes:
200202
redis-data:
201203
prometheus-data:
202204

205+
203206
secrets:
204207
dask_tls_ca:
205208
file: ${DASK_TLS_CA_FILE}

services/clusters-keeper/src/simcore_service_clusters_keeper/utils/clusters.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import yaml
1010
from aws_library.ec2 import EC2InstanceBootSpecific, EC2InstanceData, EC2Tags
1111
from aws_library.ec2._models import CommandStr
12+
from common_library.json_serialization import json_dumps
13+
from common_library.serialization import model_dump_with_secrets
1214
from fastapi.encoders import jsonable_encoder
1315
from models_library.api_schemas_clusters_keeper.clusters import (
1416
ClusterState,
@@ -81,6 +83,8 @@ def _convert_to_env_list(entries: list[Any]) -> str:
8183
def _convert_to_env_dict(entries: dict[str, Any]) -> str:
8284
return f"'{json.dumps(jsonable_encoder(entries))}'"
8385

86+
assert app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES # nosec
87+
8488
return [
8589
f"CLUSTERS_KEEPER_EC2_ACCESS_KEY_ID={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ACCESS_KEY_ID}",
8690
f"CLUSTERS_KEEPER_EC2_ENDPOINT={app_settings.CLUSTERS_KEEPER_EC2_ACCESS.EC2_ENDPOINT or 'null'}",
@@ -102,6 +106,7 @@ def _convert_to_env_dict(entries: dict[str, Any]) -> str:
102106
f"WORKERS_EC2_INSTANCES_SUBNET_ID={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_SUBNET_ID}",
103107
f"WORKERS_EC2_INSTANCES_TIME_BEFORE_DRAINING={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_TIME_BEFORE_DRAINING}",
104108
f"WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION={app_settings.CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES.WORKERS_EC2_INSTANCES_TIME_BEFORE_TERMINATION}",
109+
f"AUTOSCALING_RABBITMQ={json_dumps(model_dump_with_secrets(app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_RABBIT, show_secrets=True)) if app_settings.CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES.PRIMARY_EC2_INSTANCES_RABBIT else 'null'}",
105110
]
106111

107112

services/clusters-keeper/tests/unit/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def disable_clusters_management_background_task(
224224
@pytest.fixture
225225
def disabled_rabbitmq(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatch):
226226
monkeypatch.setenv("CLUSTERS_KEEPER_RABBITMQ", "null")
227+
monkeypatch.delenv("RABBIT_HOST", raising=False)
227228

228229

229230
@pytest.fixture
@@ -331,7 +332,7 @@ async def _do(num: int) -> list[str]:
331332
"Tags": [
332333
{
333334
"Key": "Name",
334-
"Value": f"{get_cluster_name(app_settings,user_id=user_id,wallet_id=wallet_id,is_manager=False)}_blahblah",
335+
"Value": f"{get_cluster_name(app_settings, user_id=user_id, wallet_id=wallet_id, is_manager=False)}_blahblah",
335336
}
336337
],
337338
}

0 commit comments

Comments
 (0)