Skip to content

Commit efd3084

Browse files
author
Andrei Neagu
committed
adding docker network creation interface
1 parent ad614b1 commit efd3084

File tree

8 files changed

+175
-3
lines changed

8 files changed

+175
-3
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from .projects_nodes_io import NodeID
2222
from .users import UserID
2323

24+
DockerNetworkID: TypeAlias = str
25+
2426

2527
class DockerLabelKey(ConstrainedStr):
2628
# NOTE: https://docs.docker.com/config/labels-custom-metadata/#key-format-recommendations
@@ -37,7 +39,15 @@ def from_key(cls, key: str) -> "DockerLabelKey":
3739
str, StringConstraints(pattern=DOCKER_GENERIC_TAG_KEY_RE)
3840
]
3941

40-
DockerPlacementConstraint: TypeAlias = Annotated[str, StringConstraints(strip_whitespace = True, pattern = re.compile(r"^(?!-)(?![.])(?!.*--)(?!.*[.][.])[a-zA-Z0-9.-]*(?<!-)(?<![.])(!=|==)[a-zA-Z0-9_. -]*$"))]
42+
DockerPlacementConstraint: TypeAlias = Annotated[
43+
str,
44+
StringConstraints(
45+
strip_whitespace=True,
46+
pattern=re.compile(
47+
r"^(?!-)(?![.])(?!.*--)(?!.*[.][.])[a-zA-Z0-9.-]*(?<!-)(?<![.])(!=|==)[a-zA-Z0-9_. -]*$"
48+
),
49+
),
50+
]
4151

4252
_SIMCORE_RUNTIME_DOCKER_LABEL_PREFIX: Final[str] = "io.simcore.runtime."
4353
_BACKWARDS_COMPATIBILITY_SIMCORE_RUNTIME_DOCKER_LABELS_MAP: Final[dict[str, str]] = {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from aiodocker import Docker
2+
from fastapi import Request
3+
4+
from ...modules.docker_client import SharedDockerClient
5+
6+
7+
def get_shared_docker_client(request: Request) -> Docker:
8+
return SharedDockerClient.docker_instance(request.app)

services/director-v2/src/simcore_service_director_v2/api/entrypoints.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .routes import (
55
computations,
66
computations_tasks,
7+
docker_networks,
78
dynamic_scheduler,
89
dynamic_services,
910
health,
@@ -26,6 +27,9 @@
2627
v2_router.include_router(
2728
dynamic_services.router, tags=["dynamic services"], prefix="/dynamic_services"
2829
)
30+
v2_router.include_router(
31+
docker_networks.router, tags=["docker networks"], prefix="/docker/networks"
32+
)
2933

3034
v2_router.include_router(
3135
dynamic_scheduler.router, tags=["dynamic scheduler"], prefix="/dynamic_scheduler"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from typing import Annotated
2+
3+
from aiodocker import Docker
4+
from fastapi import APIRouter, Depends, status
5+
from models_library.docker import DockerNetworkID
6+
from models_library.generated_models.docker_rest_api import Network
7+
8+
from ..dependencies.docker import get_shared_docker_client
9+
10+
router = APIRouter()
11+
12+
13+
@router.post(
14+
"/",
15+
summary="create a docker network given the input parameters",
16+
status_code=status.HTTP_200_OK,
17+
)
18+
async def create_docker_network(
19+
docker_network: Network,
20+
shared_docker_client: Annotated[Docker, Depends(get_shared_docker_client)],
21+
) -> DockerNetworkID:
22+
created_network = await shared_docker_client.networks.create(
23+
docker_network.model_dump(mode="json")
24+
)
25+
return created_network.id
26+
27+
28+
@router.delete(
29+
"/{network_id}",
30+
summary="removes an existing docker network",
31+
status_code=status.HTTP_204_NO_CONTENT,
32+
)
33+
async def remove_docker_network(
34+
network_id: DockerNetworkID,
35+
shared_docker_client: Annotated[Docker, Depends(get_shared_docker_client)],
36+
):
37+
created_network = await shared_docker_client.networks.get(network_id)
38+
await created_network.delete()

services/director-v2/src/simcore_service_director_v2/core/application.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
dask_clients_pool,
2424
db,
2525
director_v0,
26+
docker_client,
2627
dynamic_services,
2728
dynamic_sidecar,
2829
instrumentation,
@@ -206,6 +207,8 @@ def init_app(settings: AppSettings | None = None) -> FastAPI:
206207
if settings.DIRECTOR_V2_PROFILING:
207208
app.add_middleware(ProfilerMiddleware)
208209

210+
docker_client.setup(app)
211+
209212
# setup app --
210213
app.add_event_handler("startup", on_startup)
211214
app.add_event_handler("shutdown", on_shutdown)

services/director-v2/src/simcore_service_director_v2/core/dynamic_services_settings/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ class DynamicServicesSettings(BaseCustomSettings):
1313
default=True, description="Enables/Disables the dynamic_sidecar submodule"
1414
)
1515

16-
DYNAMIC_SIDECAR: DynamicSidecarSettings = Field(json_schema_extra={"auto_default_from_env": True})
16+
DYNAMIC_SIDECAR: DynamicSidecarSettings = Field(
17+
json_schema_extra={"auto_default_from_env": True}
18+
)
1719

1820
DYNAMIC_SCHEDULER: DynamicServicesSchedulerSettings = Field(
1921
json_schema_extra={"auto_default_from_env": True}
@@ -31,4 +33,6 @@ class DynamicServicesSettings(BaseCustomSettings):
3133
json_schema_extra={"auto_default_from_env": True}
3234
)
3335

34-
WEBSERVER_SETTINGS: WebServerSettings = Field(json_schema_extra={"auto_default_from_env": True})
36+
WEBSERVER_SETTINGS: WebServerSettings = Field(
37+
json_schema_extra={"auto_default_from_env": True}
38+
)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from dataclasses import dataclass
2+
3+
import aiodocker
4+
from aiodocker import Docker
5+
from fastapi import FastAPI
6+
from servicelib.fastapi.app_state import SingletonInAppStateMixin
7+
8+
9+
@dataclass
10+
class SharedDockerClient(SingletonInAppStateMixin):
11+
app_state_name: str = "shared_docker_client"
12+
13+
docker_client: Docker | None = None
14+
15+
@classmethod
16+
def docker_instance(cls, app: FastAPI) -> Docker:
17+
docker_client = cls.get_from_app_state(app).docker_client
18+
assert docker_client is not None # nosec
19+
return docker_client
20+
21+
async def setup(self) -> None:
22+
self.docker_client = aiodocker.Docker()
23+
24+
async def shutdown(self) -> None:
25+
if self.docker_client is not None:
26+
await self.docker_client.close()
27+
28+
29+
def setup(app: FastAPI) -> None:
30+
async def on_startup() -> None:
31+
shared_client = SharedDockerClient()
32+
await shared_client.setup()
33+
shared_client.set_to_app_state(app)
34+
35+
async def on_shutdown() -> None:
36+
shared_client = SharedDockerClient.pop_from_app_state(app)
37+
await shared_client.shutdown()
38+
39+
app.add_event_handler("startup", on_startup)
40+
app.add_event_handler("shutdown", on_shutdown)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
4+
from collections.abc import AsyncIterable
5+
6+
import pytest
7+
from aiodocker import Docker, DockerError
8+
from faker import Faker
9+
from fastapi import status
10+
from models_library.docker import DockerNetworkID
11+
from models_library.generated_models.docker_rest_api import Network
12+
from pytest_simcore.helpers.typing_env import EnvVarsDict
13+
from starlette.testclient import TestClient
14+
15+
16+
@pytest.fixture
17+
def mock_env(
18+
mock_exclusive: None,
19+
disable_rabbitmq: None,
20+
disable_postgres: None,
21+
mock_env: EnvVarsDict,
22+
monkeypatch: pytest.MonkeyPatch,
23+
faker: Faker,
24+
) -> None:
25+
monkeypatch.setenv("DIRECTOR_V2_DOCKER_ENTRYPOINT_ACCESS_TOKEN", "adminadmin")
26+
27+
monkeypatch.setenv("SC_BOOT_MODE", "default")
28+
monkeypatch.setenv("DIRECTOR_ENABLED", "false")
29+
monkeypatch.setenv("COMPUTATIONAL_BACKEND_ENABLED", "false")
30+
monkeypatch.setenv("COMPUTATIONAL_BACKEND_DASK_CLIENT_ENABLED", "false")
31+
32+
monkeypatch.setenv("DIRECTOR_V2_DYNAMIC_SCHEDULER_ENABLED", "false")
33+
34+
monkeypatch.setenv("R_CLONE_PROVIDER", "MINIO")
35+
monkeypatch.setenv("S3_ENDPOINT", faker.url())
36+
monkeypatch.setenv("S3_ACCESS_KEY", faker.pystr())
37+
monkeypatch.setenv("S3_REGION", faker.pystr())
38+
monkeypatch.setenv("S3_SECRET_KEY", faker.pystr())
39+
monkeypatch.setenv("S3_BUCKET_NAME", faker.pystr())
40+
41+
42+
@pytest.fixture
43+
async def docker_client() -> AsyncIterable[Docker]:
44+
async with Docker() as client:
45+
yield client
46+
47+
48+
async def test_routes_are_protected(client: TestClient, docker_client: Docker):
49+
network_name = "a_test_network"
50+
network = Network(name=network_name)
51+
52+
response = client.post("/v2/docker/networks/", json=network.model_dump(mode="json"))
53+
assert response.status_code == status.HTTP_200_OK, response.text
54+
network_id: DockerNetworkID = response.json()
55+
56+
response = client.delete(f"/v2/docker/networks/{network_id}")
57+
assert response.status_code == status.HTTP_204_NO_CONTENT, response.text
58+
59+
# check it is not here
60+
61+
for name_or_id in (network_name, network_id):
62+
with pytest.raises(DockerError) as exc:
63+
await docker_client.networks.get(name_or_id)
64+
65+
assert exc.value.status == status.HTTP_404_NOT_FOUND

0 commit comments

Comments
 (0)