Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions netengine/handlers/substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Emit substrate.initialized event on success
"""

import ipaddress
from datetime import UTC, datetime
from typing import Any, cast

Expand Down Expand Up @@ -294,6 +295,24 @@ async def _ensure_docker_network(
def _sync() -> str:
try:
net = client.networks.get(name)
ipam_config = net.attrs.get("IPAM", {}).get("Config") or []
configured_subnets = [
config["Subnet"]
for config in ipam_config
if isinstance(config, dict) and config.get("Subnet")
]
requested_network = ipaddress.ip_network(subnet, strict=False)
matching_subnet = any(
ipaddress.ip_network(configured_subnet, strict=False) == requested_network
for configured_subnet in configured_subnets
)
if not matching_subnet:
actual = ", ".join(configured_subnets) if configured_subnets else "none"
raise SubstrateError(
f"Docker network '{name}' already exists but uses subnet(s) {actual}; "
f"expected {subnet}. Run docker network rm {name} or choose a "
f"different subnet in the spec."
)
return net.id
except docker_lib.errors.NotFound:
try:
Expand Down
73 changes: 72 additions & 1 deletion tests/test_m1_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
"""

from datetime import UTC, datetime
from unittest.mock import MagicMock

from netengine.errors import DNSError
import pytest

from netengine.errors import DNSError, SubstrateError
from netengine.handlers.context import PhaseContext
from netengine.handlers.dns import DNSHandler
from netengine.handlers.substrate import SubstrateHandler
Expand Down Expand Up @@ -95,6 +98,74 @@ async def test_should_skip_false_before_execute(

assert skip is False

async def test_ensure_docker_network_reuses_existing_matching_subnet(
self, phase_context_substrate: PhaseContext
) -> None:
"""Existing Docker network should be reused when its subnet matches the spec."""
handler = SubstrateHandler()
docker_adapter = MagicMock()
docker_client = MagicMock()
network = MagicMock()
network.id = "existing-core-id"
network.attrs = {"IPAM": {"Config": [{"Subnet": "10.0.0.42/24"}]}}
docker_client.networks.get.return_value = network
docker_adapter.client = docker_client
phase_context_substrate.docker_client = docker_adapter

net_id = await handler._ensure_docker_network(
phase_context_substrate, "core", "10.0.0.0/24", "bridge"
)

assert net_id == "existing-core-id"
docker_client.networks.create.assert_not_called()

async def test_ensure_docker_network_rejects_existing_mismatched_subnet(
self, phase_context_substrate: PhaseContext
) -> None:
"""Existing Docker network should fail fast when its subnet differs from the spec."""
handler = SubstrateHandler()
docker_adapter = MagicMock()
docker_client = MagicMock()
network = MagicMock()
network.id = "existing-core-id"
network.attrs = {"IPAM": {"Config": [{"Subnet": "10.1.0.0/24"}]}}
docker_client.networks.get.return_value = network
docker_adapter.client = docker_client
phase_context_substrate.docker_client = docker_adapter

with pytest.raises(SubstrateError, match="already exists but uses subnet") as exc_info:
await handler._ensure_docker_network(
phase_context_substrate, "core", "10.0.0.0/24", "bridge"
)

assert "10.1.0.0/24" in str(exc_info.value)
assert "expected 10.0.0.0/24" in str(exc_info.value)
assert "docker network rm core" in str(exc_info.value)
docker_client.networks.create.assert_not_called()

async def test_ensure_docker_network_rejects_existing_missing_ipam_config(
self, phase_context_substrate: PhaseContext
) -> None:
"""Existing Docker network should fail when Docker reports no configured subnets."""
handler = SubstrateHandler()
docker_adapter = MagicMock()
docker_client = MagicMock()
network = MagicMock()
network.id = "existing-core-id"
network.attrs = {"IPAM": {}}
docker_client.networks.get.return_value = network
docker_adapter.client = docker_client
phase_context_substrate.docker_client = docker_adapter

with pytest.raises(SubstrateError) as exc_info:
await handler._ensure_docker_network(
phase_context_substrate, "core", "10.0.0.0/24", "bridge"
)

assert "uses subnet(s) none" in str(exc_info.value)
assert "expected 10.0.0.0/24" in str(exc_info.value)
docker_client.networks.create.assert_not_called()


class TestDNSHandler:
"""Phases 1-2: DNS handler tests."""
Expand Down