From bf01e33325bcead72cdcc2879104f0db35fbf647 Mon Sep 17 00:00:00 2001 From: aidankhogg Date: Mon, 29 Jun 2026 22:46:22 +0100 Subject: [PATCH] Validate existing Docker network subnets --- netengine/handlers/substrate.py | 19 +++++++++ tests/test_m1_handlers.py | 73 ++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/netengine/handlers/substrate.py b/netengine/handlers/substrate.py index 567e405..e772f49 100644 --- a/netengine/handlers/substrate.py +++ b/netengine/handlers/substrate.py @@ -8,6 +8,7 @@ - Emit substrate.initialized event on success """ +import ipaddress from datetime import UTC, datetime from typing import Any, cast @@ -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: diff --git a/tests/test_m1_handlers.py b/tests/test_m1_handlers.py index ba58c8f..759c06c 100644 --- a/tests/test_m1_handlers.py +++ b/tests/test_m1_handlers.py @@ -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 @@ -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."""