|
1 | 1 | # pylint:disable=unused-argument |
2 | 2 | # pylint:disable=redefined-outer-name |
3 | 3 | # pylint:disable=protected-access |
4 | | - |
5 | 4 | import asyncio |
| 5 | +from collections.abc import AsyncIterable |
| 6 | +from inspect import signature |
6 | 7 | from typing import Final |
7 | 8 | from unittest.mock import AsyncMock |
8 | 9 |
|
| 10 | +import aiodocker |
9 | 11 | import pytest |
| 12 | +import yaml |
| 13 | +from aiodocker.volumes import DockerVolume |
| 14 | +from faker import Faker |
10 | 15 | 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 |
11 | 21 | from models_library.services import ServiceOutput |
| 22 | +from models_library.services_creation import CreateServiceMetricsAdditionalParams |
12 | 23 | from pydantic import TypeAdapter |
13 | 24 | from pytest_mock import MockerFixture |
| 25 | +from servicelib.fastapi.long_running_tasks._manager import FastAPILongRunningManager |
| 26 | +from servicelib.long_running_tasks.models import LRTNamespace |
14 | 27 | 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 |
17 | 34 | from simcore_service_dynamic_sidecar.core.settings import ApplicationSettings |
18 | 35 | from simcore_service_dynamic_sidecar.modules.inputs import InputsState |
19 | 36 | from simcore_service_dynamic_sidecar.modules.outputs._watcher import OutputsWatcher |
| 37 | +from utils import get_lrt_result |
20 | 38 |
|
21 | 39 | pytest_simcore_core_services_selection = [ |
22 | 40 | "rabbit", |
@@ -131,3 +149,205 @@ async def test_container_create_outputs_dirs( |
131 | 149 | mock_event_filter_enqueue.call_count |
132 | 150 | == EXPECT_EVENTS_WHEN_CREATING_OUTPUT_PORT_KEY_DIRS |
133 | 151 | ) |
| 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