Skip to content

Commit bf01e33

Browse files
committed
Validate existing Docker network subnets
1 parent e477c64 commit bf01e33

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

netengine/handlers/substrate.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Emit substrate.initialized event on success
99
"""
1010

11+
import ipaddress
1112
from datetime import UTC, datetime
1213
from typing import Any, cast
1314

@@ -294,6 +295,24 @@ async def _ensure_docker_network(
294295
def _sync() -> str:
295296
try:
296297
net = client.networks.get(name)
298+
ipam_config = net.attrs.get("IPAM", {}).get("Config") or []
299+
configured_subnets = [
300+
config["Subnet"]
301+
for config in ipam_config
302+
if isinstance(config, dict) and config.get("Subnet")
303+
]
304+
requested_network = ipaddress.ip_network(subnet, strict=False)
305+
matching_subnet = any(
306+
ipaddress.ip_network(configured_subnet, strict=False) == requested_network
307+
for configured_subnet in configured_subnets
308+
)
309+
if not matching_subnet:
310+
actual = ", ".join(configured_subnets) if configured_subnets else "none"
311+
raise SubstrateError(
312+
f"Docker network '{name}' already exists but uses subnet(s) {actual}; "
313+
f"expected {subnet}. Run docker network rm {name} or choose a "
314+
f"different subnet in the spec."
315+
)
297316
return net.id
298317
except docker_lib.errors.NotFound:
299318
try:

tests/test_m1_handlers.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
"""
55

66
from datetime import UTC, datetime
7+
from unittest.mock import MagicMock
78

8-
from netengine.errors import DNSError
9+
import pytest
10+
11+
from netengine.errors import DNSError, SubstrateError
912
from netengine.handlers.context import PhaseContext
1013
from netengine.handlers.dns import DNSHandler
1114
from netengine.handlers.substrate import SubstrateHandler
@@ -95,6 +98,74 @@ async def test_should_skip_false_before_execute(
9598

9699
assert skip is False
97100

101+
async def test_ensure_docker_network_reuses_existing_matching_subnet(
102+
self, phase_context_substrate: PhaseContext
103+
) -> None:
104+
"""Existing Docker network should be reused when its subnet matches the spec."""
105+
handler = SubstrateHandler()
106+
docker_adapter = MagicMock()
107+
docker_client = MagicMock()
108+
network = MagicMock()
109+
network.id = "existing-core-id"
110+
network.attrs = {"IPAM": {"Config": [{"Subnet": "10.0.0.42/24"}]}}
111+
docker_client.networks.get.return_value = network
112+
docker_adapter.client = docker_client
113+
phase_context_substrate.docker_client = docker_adapter
114+
115+
net_id = await handler._ensure_docker_network(
116+
phase_context_substrate, "core", "10.0.0.0/24", "bridge"
117+
)
118+
119+
assert net_id == "existing-core-id"
120+
docker_client.networks.create.assert_not_called()
121+
122+
async def test_ensure_docker_network_rejects_existing_mismatched_subnet(
123+
self, phase_context_substrate: PhaseContext
124+
) -> None:
125+
"""Existing Docker network should fail fast when its subnet differs from the spec."""
126+
handler = SubstrateHandler()
127+
docker_adapter = MagicMock()
128+
docker_client = MagicMock()
129+
network = MagicMock()
130+
network.id = "existing-core-id"
131+
network.attrs = {"IPAM": {"Config": [{"Subnet": "10.1.0.0/24"}]}}
132+
docker_client.networks.get.return_value = network
133+
docker_adapter.client = docker_client
134+
phase_context_substrate.docker_client = docker_adapter
135+
136+
with pytest.raises(SubstrateError, match="already exists but uses subnet") as exc_info:
137+
await handler._ensure_docker_network(
138+
phase_context_substrate, "core", "10.0.0.0/24", "bridge"
139+
)
140+
141+
assert "10.1.0.0/24" in str(exc_info.value)
142+
assert "expected 10.0.0.0/24" in str(exc_info.value)
143+
assert "docker network rm core" in str(exc_info.value)
144+
docker_client.networks.create.assert_not_called()
145+
146+
async def test_ensure_docker_network_rejects_existing_missing_ipam_config(
147+
self, phase_context_substrate: PhaseContext
148+
) -> None:
149+
"""Existing Docker network should fail when Docker reports no configured subnets."""
150+
handler = SubstrateHandler()
151+
docker_adapter = MagicMock()
152+
docker_client = MagicMock()
153+
network = MagicMock()
154+
network.id = "existing-core-id"
155+
network.attrs = {"IPAM": {}}
156+
docker_client.networks.get.return_value = network
157+
docker_adapter.client = docker_client
158+
phase_context_substrate.docker_client = docker_adapter
159+
160+
with pytest.raises(SubstrateError) as exc_info:
161+
await handler._ensure_docker_network(
162+
phase_context_substrate, "core", "10.0.0.0/24", "bridge"
163+
)
164+
165+
assert "uses subnet(s) none" in str(exc_info.value)
166+
assert "expected 10.0.0.0/24" in str(exc_info.value)
167+
docker_client.networks.create.assert_not_called()
168+
98169

99170
class TestDNSHandler:
100171
"""Phases 1-2: DNS handler tests."""

0 commit comments

Comments
 (0)