Skip to content

Commit 5bee321

Browse files
author
Andrei Neagu
committed
added API for attach/detach container extentions
1 parent f9e757f commit 5bee321

File tree

3 files changed

+283
-3
lines changed

3 files changed

+283
-3
lines changed

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/dynamic_sidecar/container_extensions.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,41 @@ async def create_output_dirs(
4444
outputs_labels=outputs_labels,
4545
)
4646
assert result is None # nosec
47+
48+
49+
@log_decorator(_logger, level=logging.DEBUG)
50+
async def attach_container_to_network(
51+
rabbitmq_rpc_client: RabbitMQRPCClient,
52+
*,
53+
node_id: NodeID,
54+
container_id: str,
55+
network_id: str,
56+
network_aliases: list[str]
57+
) -> None:
58+
rpc_namespace = get_rpc_namespace(node_id)
59+
result = await rabbitmq_rpc_client.request(
60+
rpc_namespace,
61+
TypeAdapter(RPCMethodName).validate_python("attach_container_to_network"),
62+
container_id=container_id,
63+
network_id=network_id,
64+
network_aliases=network_aliases,
65+
)
66+
assert result is None # nosec
67+
68+
69+
@log_decorator(_logger, level=logging.DEBUG)
70+
async def detach_container_from_network(
71+
rabbitmq_rpc_client: RabbitMQRPCClient,
72+
*,
73+
node_id: NodeID,
74+
container_id: str,
75+
network_id: str
76+
) -> None:
77+
rpc_namespace = get_rpc_namespace(node_id)
78+
result = await rabbitmq_rpc_client.request(
79+
rpc_namespace,
80+
TypeAdapter(RPCMethodName).validate_python("detach_container_from_network"),
81+
container_id=container_id,
82+
network_id=network_id,
83+
)
84+
assert result is None # nosec

services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/api/rpc/_containers_extension.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,25 @@ async def create_output_dirs(
2121
app: FastAPI, *, outputs_labels: dict[str, ServiceOutput]
2222
) -> None:
2323
await container_extensions.create_output_dirs(app, outputs_labels=outputs_labels)
24+
25+
26+
@router.expose()
27+
async def attach_container_to_network(
28+
app: FastAPI, *, container_id: str, network_id: str, network_aliases: list[str]
29+
) -> None:
30+
_ = app
31+
await container_extensions.attach_container_to_network(
32+
container_id=container_id,
33+
network_id=network_id,
34+
network_aliases=network_aliases,
35+
)
36+
37+
38+
@router.expose()
39+
async def detach_container_from_network(
40+
app: FastAPI, *, container_id: str, network_id: str
41+
) -> None:
42+
_ = app
43+
await container_extensions.detach_container_from_network(
44+
container_id=container_id, network_id=network_id
45+
)

services/dynamic-sidecar/tests/unit/api/rpc/test__container_extensions.py

Lines changed: 223 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
# pylint:disable=unused-argument
22
# pylint:disable=redefined-outer-name
33
# pylint:disable=protected-access
4-
54
import asyncio
5+
from collections.abc import AsyncIterable
6+
from inspect import signature
67
from typing import Final
78
from unittest.mock import AsyncMock
89

10+
import aiodocker
911
import pytest
12+
import yaml
13+
from aiodocker.volumes import DockerVolume
14+
from faker import Faker
1015
from fastapi import FastAPI
16+
from models_library.api_schemas_directorv2.dynamic_services import (
17+
ContainersComposeSpec,
18+
ContainersCreate,
19+
)
20+
from models_library.projects_nodes_io import NodeID
1121
from models_library.services import ServiceOutput
22+
from models_library.services_creation import CreateServiceMetricsAdditionalParams
1223
from pydantic import TypeAdapter
1324
from pytest_mock import MockerFixture
25+
from servicelib.fastapi.long_running_tasks._manager import FastAPILongRunningManager
26+
from servicelib.long_running_tasks.models import LRTNamespace
1427
from servicelib.rabbitmq import RabbitMQRPCClient
15-
from servicelib.rabbitmq.rpc_interfaces.dynamic_sidecar import container_extensions
16-
from simcore_service_dynamic_sidecar.core.application import AppState
28+
from servicelib.rabbitmq.rpc_interfaces.dynamic_sidecar import (
29+
container_extensions,
30+
containers,
31+
containers_long_running_tasks,
32+
)
33+
from simcore_service_dynamic_sidecar.core.application import AppState, SharedStore
1734
from simcore_service_dynamic_sidecar.core.settings import ApplicationSettings
1835
from simcore_service_dynamic_sidecar.modules.inputs import InputsState
1936
from simcore_service_dynamic_sidecar.modules.outputs._watcher import OutputsWatcher
37+
from utils import get_lrt_result
2038

2139
pytest_simcore_core_services_selection = [
2240
"rabbit",
@@ -131,3 +149,205 @@ async def test_container_create_outputs_dirs(
131149
mock_event_filter_enqueue.call_count
132150
== EXPECT_EVENTS_WHEN_CREATING_OUTPUT_PORT_KEY_DIRS
133151
)
152+
153+
154+
@pytest.fixture
155+
async def attachable_networks_and_ids(faker: Faker) -> AsyncIterable[dict[str, str]]:
156+
# generate some network names
157+
unique_id = faker.uuid4()
158+
network_names = {f"test_network_{i}_{unique_id}": "" for i in range(10)}
159+
160+
# create networks
161+
async with aiodocker.Docker() as client:
162+
for network_name in network_names:
163+
network_config = {
164+
"Name": network_name,
165+
"Driver": "overlay",
166+
"Attachable": True,
167+
"Internal": True,
168+
}
169+
network = await client.networks.create(network_config)
170+
network_names[network_name] = network.id
171+
172+
yield network_names
173+
174+
# remove networks
175+
async with aiodocker.Docker() as client:
176+
for network_id in network_names.values():
177+
network = await client.networks.get(network_id)
178+
assert await network.delete() is True
179+
180+
181+
@pytest.fixture
182+
def dynamic_sidecar_network_name() -> str:
183+
return "entrypoint_container_network"
184+
185+
186+
@pytest.fixture
187+
def compose_spec(dynamic_sidecar_network_name: str) -> ContainersComposeSpec:
188+
return ContainersComposeSpec(
189+
docker_compose_yaml=yaml.dump(
190+
{
191+
"version": "3",
192+
"services": {
193+
"first-box": {
194+
"image": "busybox:latest",
195+
"networks": {
196+
dynamic_sidecar_network_name: None,
197+
},
198+
"labels": {"io.osparc.test-label": "mark-entrypoint"},
199+
},
200+
"second-box": {"image": "busybox:latest"},
201+
"egress": {
202+
"image": "busybox:latest",
203+
"networks": {
204+
dynamic_sidecar_network_name: None,
205+
},
206+
},
207+
},
208+
"networks": {dynamic_sidecar_network_name: None},
209+
}
210+
)
211+
)
212+
213+
214+
@pytest.fixture
215+
def compose_spec_single_service() -> ContainersComposeSpec:
216+
return ContainersComposeSpec(
217+
docker_compose_yaml=yaml.dump(
218+
{
219+
"version": "3",
220+
"services": {
221+
"solo-box": {
222+
"image": "busybox:latest",
223+
"labels": {"io.osparc.test-label": "mark-entrypoint"},
224+
},
225+
},
226+
}
227+
)
228+
)
229+
230+
231+
@pytest.fixture(params=["compose_spec", "compose_spec_single_service"])
232+
def selected_spec(
233+
request, compose_spec: str, compose_spec_single_service: str
234+
) -> ContainersComposeSpec:
235+
# check that fixture_name is present in this function's parameters
236+
fixture_name = request.param
237+
sig = signature(selected_spec)
238+
assert fixture_name in sig.parameters, (
239+
f"Provided fixture name {fixture_name} was not found "
240+
f"as a parameter in the signature {sig}"
241+
)
242+
243+
# returns the parameter by name from the ones declared in the signature
244+
result: ContainersComposeSpec = locals()[fixture_name]
245+
return result
246+
247+
248+
@pytest.fixture
249+
def lrt_namespace(app: FastAPI) -> LRTNamespace:
250+
long_running_manager: FastAPILongRunningManager = app.state.long_running_manager
251+
return long_running_manager.lrt_namespace
252+
253+
254+
_FAST_STATUS_POLL: Final[float] = 0.1
255+
_CREATE_SERVICE_CONTAINERS_TIMEOUT: Final[float] = 60
256+
257+
258+
async def _start_containers(
259+
app: FastAPI,
260+
rpc_client: RabbitMQRPCClient,
261+
node_id: NodeID,
262+
lrt_namespace: LRTNamespace,
263+
compose_spec: ContainersComposeSpec,
264+
mock_metrics_params: CreateServiceMetricsAdditionalParams,
265+
) -> list[str]:
266+
await containers.create_compose_spec(
267+
rpc_client, node_id=node_id, containers_compose_spec=compose_spec
268+
)
269+
270+
containers_create = ContainersCreate(metrics_params=mock_metrics_params)
271+
task_id = await containers_long_running_tasks.create_user_services(
272+
rpc_client,
273+
node_id=node_id,
274+
lrt_namespace=lrt_namespace,
275+
containers_create=containers_create,
276+
)
277+
278+
response_containers = await get_lrt_result(
279+
rpc_client,
280+
lrt_namespace,
281+
task_id,
282+
status_poll_interval=_FAST_STATUS_POLL,
283+
task_timeout=_CREATE_SERVICE_CONTAINERS_TIMEOUT,
284+
)
285+
286+
shared_store: SharedStore = app.state.shared_store
287+
container_names = shared_store.container_names
288+
assert response_containers == container_names
289+
290+
return container_names
291+
292+
293+
def _create_network_aliases(network_name: str) -> list[str]:
294+
return [f"alias_{i}_{network_name}" for i in range(10)]
295+
296+
297+
async def test_attach_detach_container_to_network(
298+
ensure_external_volumes: tuple[DockerVolume],
299+
docker_swarm: None,
300+
app: FastAPI,
301+
rpc_client: RabbitMQRPCClient,
302+
lrt_namespace: LRTNamespace,
303+
selected_spec: ContainersComposeSpec,
304+
attachable_networks_and_ids: dict[str, str],
305+
mock_metrics_params: CreateServiceMetricsAdditionalParams,
306+
):
307+
app_state = AppState(app)
308+
309+
container_names = await _start_containers(
310+
app,
311+
rpc_client,
312+
node_id=app_state.settings.DY_SIDECAR_NODE_ID,
313+
lrt_namespace=lrt_namespace,
314+
compose_spec=selected_spec,
315+
mock_metrics_params=mock_metrics_params,
316+
)
317+
318+
async with aiodocker.Docker() as docker:
319+
for container_name in container_names:
320+
for network_name, network_id in attachable_networks_and_ids.items():
321+
network_aliases = _create_network_aliases(network_name)
322+
323+
# attach network to containers
324+
for _ in range(2): # calling 2 times in a row
325+
await container_extensions.attach_container_to_network(
326+
rpc_client,
327+
node_id=app_state.settings.DY_SIDECAR_NODE_ID,
328+
container_id=container_name,
329+
network_id=network_id,
330+
network_aliases=network_aliases,
331+
)
332+
333+
container = await docker.containers.get(container_name)
334+
container_inspect = await container.show()
335+
networks = container_inspect["NetworkSettings"]["Networks"]
336+
assert network_id in networks
337+
assert set(network_aliases).issubset(
338+
set(networks[network_id]["Aliases"])
339+
)
340+
341+
# detach network from containers
342+
for _ in range(2): # running twice in a row
343+
await container_extensions.detach_container_from_network(
344+
rpc_client,
345+
node_id=app_state.settings.DY_SIDECAR_NODE_ID,
346+
container_id=container_name,
347+
network_id=network_id,
348+
)
349+
350+
container = await docker.containers.get(container_name)
351+
container_inspect = await container.show()
352+
networks = container_inspect["NetworkSettings"]["Networks"]
353+
assert network_id not in networks

0 commit comments

Comments
 (0)