Skip to content

Commit ad20e4b

Browse files
sandereggpcrespov
authored andcommitted
🐛Autoscaling: fix listing of nodes with common prefix (#4410)
1 parent 7afe235 commit ad20e4b

File tree

4 files changed

+95
-38
lines changed

4 files changed

+95
-38
lines changed

services/autoscaling/src/simcore_service_autoscaling/utils/utils_docker.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,14 @@ async def find_node_with_name(
414414
list_of_nodes = await docker_client.nodes.list(filters={"name": name})
415415
if not list_of_nodes:
416416
return None
417-
return parse_obj_as(Node, list_of_nodes[0])
417+
# note that there might be several nodes with a common_prefixed name. so now we want exact matching
418+
list_of_nodes = parse_obj_as(list[Node], list_of_nodes)
419+
for node in list_of_nodes:
420+
assert node.Description # nosec
421+
if node.Description.Hostname == name:
422+
return node
423+
424+
return None
418425

419426

420427
async def tag_node(

services/autoscaling/tests/unit/conftest.py

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,7 @@
77
import random
88
from datetime import timezone
99
from pathlib import Path
10-
from typing import (
11-
Any,
12-
AsyncIterator,
13-
Awaitable,
14-
Callable,
15-
Final,
16-
Iterator,
17-
Optional,
18-
Union,
19-
cast,
20-
)
10+
from typing import Any, AsyncIterator, Awaitable, Callable, Final, Iterator, cast
2111

2212
import aiodocker
2313
import httpx
@@ -186,7 +176,6 @@ def service_monitored_labels(
186176

187177
@pytest.fixture
188178
async def async_client(initialized_app: FastAPI) -> AsyncIterator[httpx.AsyncClient]:
189-
190179
async with httpx.AsyncClient(
191180
app=initialized_app,
192181
base_url=f"http://{initialized_app.title}.testserver.io",
@@ -218,25 +207,35 @@ async def host_node(
218207

219208

220209
@pytest.fixture
221-
def fake_node(faker: Faker) -> Node:
222-
return Node(
223-
ID=faker.uuid4(),
224-
Version=ObjectVersion(Index=faker.pyint()),
225-
CreatedAt=faker.date_time(tzinfo=timezone.utc).isoformat(),
226-
UpdatedAt=faker.date_time(tzinfo=timezone.utc).isoformat(),
227-
Description=NodeDescription(
228-
Hostname=faker.pystr(),
229-
Resources=ResourceObject(
230-
NanoCPUs=int(9 * 1e9), MemoryBytes=256 * 1024 * 1024 * 1024
210+
def create_fake_node(faker: Faker) -> Callable[..., Node]:
211+
def _creator(**node_overrides) -> Node:
212+
default_config = dict(
213+
ID=faker.uuid4(),
214+
Version=ObjectVersion(Index=faker.pyint()),
215+
CreatedAt=faker.date_time(tzinfo=timezone.utc).isoformat(),
216+
UpdatedAt=faker.date_time(tzinfo=timezone.utc).isoformat(),
217+
Description=NodeDescription(
218+
Hostname=faker.pystr(),
219+
Resources=ResourceObject(
220+
NanoCPUs=int(9 * 1e9), MemoryBytes=256 * 1024 * 1024 * 1024
221+
),
231222
),
232-
),
233-
Spec=NodeSpec(
234-
Name=None,
235-
Labels=None,
236-
Role=None,
237-
Availability=Availability.drain,
238-
),
239-
)
223+
Spec=NodeSpec(
224+
Name=None,
225+
Labels=None,
226+
Role=None,
227+
Availability=Availability.drain,
228+
),
229+
)
230+
default_config.update(**node_overrides)
231+
return Node(**default_config)
232+
233+
return _creator
234+
235+
236+
@pytest.fixture
237+
def fake_node(create_fake_node: Callable[..., Node]) -> Node:
238+
return create_fake_node()
240239

241240

242241
@pytest.fixture
@@ -254,7 +253,7 @@ def task_template() -> dict[str, Any]:
254253

255254
@pytest.fixture
256255
def create_task_reservations() -> Callable[[NUM_CPUS, int], dict[str, Any]]:
257-
def _creator(num_cpus: NUM_CPUS, memory: Union[ByteSize, int]) -> dict[str, Any]:
256+
def _creator(num_cpus: NUM_CPUS, memory: ByteSize | int) -> dict[str, Any]:
258257
return {
259258
"Resources": {
260259
"Reservations": {
@@ -269,7 +268,7 @@ def _creator(num_cpus: NUM_CPUS, memory: Union[ByteSize, int]) -> dict[str, Any]
269268

270269
@pytest.fixture
271270
def create_task_limits() -> Callable[[NUM_CPUS, int], dict[str, Any]]:
272-
def _creator(num_cpus: NUM_CPUS, memory: Union[ByteSize, int]) -> dict[str, Any]:
271+
def _creator(num_cpus: NUM_CPUS, memory: ByteSize | int) -> dict[str, Any]:
273272
return {
274273
"Resources": {
275274
"Limits": {
@@ -288,13 +287,13 @@ async def create_service(
288287
docker_swarm: None,
289288
faker: Faker,
290289
) -> AsyncIterator[
291-
Callable[[dict[str, Any], Optional[dict[str, str]]], Awaitable[Service]]
290+
Callable[[dict[str, Any], dict[str, str] | None], Awaitable[Service]]
292291
]:
293292
created_services = []
294293

295294
async def _creator(
296295
task_template: dict[str, Any],
297-
labels: Optional[dict[str, str]] = None,
296+
labels: dict[str, str] | None = None,
298297
wait_for_service_state="running",
299298
) -> Service:
300299
service_name = f"pytest_{faker.pystr()}"
@@ -353,6 +352,7 @@ async def _creator(
353352
await asyncio.gather(
354353
*(async_docker_client.services.delete(s.ID) for s in created_services)
355354
)
355+
356356
# wait until all tasks are gone
357357
@retry(
358358
retry=retry_if_exception_type(AssertionError),

services/autoscaling/tests/unit/test_utils_docker.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from models_library.docker import DockerGenericTag, DockerLabelKey
1717
from models_library.generated_models.docker_rest_api import (
1818
Availability,
19+
NodeDescription,
1920
NodeState,
2021
Service,
2122
Task,
@@ -790,6 +791,44 @@ async def test_try_get_node_with_name_fake(
790791
assert received_node is None
791792

792793

794+
async def test_find_node_with_name_with_common_prefixed_nodes(
795+
autoscaling_docker: AutoscalingDocker,
796+
mocker: MockerFixture,
797+
create_fake_node: Callable[..., Node],
798+
):
799+
common_prefix = "ip-10-0-1-"
800+
mocked_aiodocker = mocker.patch.object(autoscaling_docker, "nodes", autospec=True)
801+
mocked_aiodocker.list.return_value = [
802+
create_fake_node(
803+
Description=NodeDescription(Hostname=f"{common_prefix}{'1'*(i+1)}")
804+
)
805+
for i in range(3)
806+
]
807+
needed_host_name = f"{common_prefix}11"
808+
found_node = await find_node_with_name(autoscaling_docker, needed_host_name)
809+
assert found_node
810+
assert found_node.Description
811+
assert found_node.Description.Hostname == needed_host_name
812+
813+
814+
async def test_find_node_with_smaller_name_with_common_prefixed_nodes_returns_none(
815+
autoscaling_docker: AutoscalingDocker,
816+
mocker: MockerFixture,
817+
create_fake_node: Callable[..., Node],
818+
):
819+
common_prefix = "ip-10-0-1-"
820+
mocked_aiodocker = mocker.patch.object(autoscaling_docker, "nodes", autospec=True)
821+
mocked_aiodocker.list.return_value = [
822+
create_fake_node(
823+
Description=NodeDescription(Hostname=f"{common_prefix}{'1'*(i+1)}")
824+
)
825+
for i in range(3)
826+
]
827+
needed_host_name = f"{common_prefix}"
828+
found_node = await find_node_with_name(autoscaling_docker, needed_host_name)
829+
assert found_node is None
830+
831+
793832
async def test_tag_node(
794833
autoscaling_docker: AutoscalingDocker, host_node: Node, faker: Faker
795834
):

services/autoscaling/tests/unit/test_utils_dynamic_scaling.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,24 @@ def _creator(**overrides) -> Task:
5656
return _creator
5757

5858

59+
@pytest.mark.parametrize(
60+
"aws_private_dns, expected_host_name",
61+
[
62+
("ip-10-12-32-3.internal-data", "ip-10-12-32-3"),
63+
("ip-10-12-32-32.internal-data", "ip-10-12-32-32"),
64+
("ip-10-0-3-129.internal-data", "ip-10-0-3-129"),
65+
("ip-10-0-3-12.internal-data", "ip-10-0-3-12"),
66+
],
67+
)
5968
def test_node_host_name_from_ec2_private_dns(
60-
fake_ec2_instance_data: Callable[..., EC2InstanceData]
69+
fake_ec2_instance_data: Callable[..., EC2InstanceData],
70+
aws_private_dns: str,
71+
expected_host_name: str,
6172
):
6273
instance = fake_ec2_instance_data(
63-
aws_private_dns="ip-10-12-32-3.internal-data",
74+
aws_private_dns=aws_private_dns,
6475
)
65-
assert node_host_name_from_ec2_private_dns(instance) == "ip-10-12-32-3"
76+
assert node_host_name_from_ec2_private_dns(instance) == expected_host_name
6677

6778

6879
def test_node_host_name_from_ec2_private_dns_raises_with_invalid_name(

0 commit comments

Comments
 (0)