fix: resolve host IP from Docker bridge network for Rancher Desktop compatibility (fixes #561)

`--add-host host.docker.internal:host-gateway` works on Docker Desktop
and standard Docker Engine but not on Rancher Desktop, where the
`host-gateway` keyword is not recognised.

Introduce `_get_host_ip()` which reads the gateway of the Docker bridge
network (e.g. 172.17.0.1) — a portable, concrete IP that works across
Docker Desktop, Docker Engine, and Rancher Desktop.  If the network
inspect fails for any reason the function falls back to `host-gateway`,
preserving the existing behaviour for standard installations.

Also add unit tests covering the success path, subprocess failure, and
empty-output fallback.
This commit is contained in:
octo-patch 2026-04-17 11:48:57 +08:00
parent 743d074184
commit 1c245ee19c
2 changed files with 78 additions and 1 deletions

View file

@ -40,6 +40,35 @@ def docker_remove_container(container: str) -> str:
return result.returncode
def _get_host_ip() -> str:
"""Resolve the host machine's IP address reachable from inside Docker containers.
Docker Desktop (Mac/Windows) supports the ``host-gateway`` special keyword, but
Rancher Desktop and some Linux configurations do not. As a more portable
alternative we read the gateway of the default Docker bridge network, which is
the same IP that ``host-gateway`` would resolve to on standard installations.
Falls back to ``host-gateway`` so existing Docker Desktop setups are unaffected.
"""
result = subprocess.run(
[
"docker",
"network",
"inspect",
"bridge",
"--format",
"{{range .IPAM.Config}}{{.Gateway}}{{end}}",
],
capture_output=True,
text=True,
check=False,
)
ip = result.stdout.strip()
if result.returncode == 0 and ip:
return ip
return "host-gateway"
def _prepare_docker_config(plano_config_file: str) -> str:
"""Copy config to a temp file, replacing localhost with host.docker.internal.
@ -88,6 +117,8 @@ def docker_start_plano_detached(
item for volume in volume_mappings for item in ("-v", volume)
]
host_ip = _get_host_ip()
options = [
"docker",
"run",
@ -98,7 +129,7 @@ def docker_start_plano_detached(
*volume_mappings_args,
*env_args,
"--add-host",
"host.docker.internal:host-gateway",
f"host.docker.internal:{host_ip}",
PLANO_DOCKER_IMAGE,
]

View file

@ -0,0 +1,46 @@
import subprocess
from unittest import mock
from planoai.docker_cli import _get_host_ip
def test_get_host_ip_returns_bridge_gateway():
"""When docker network inspect succeeds, the bridge gateway IP is returned."""
fake_result = mock.Mock()
fake_result.returncode = 0
fake_result.stdout = "172.17.0.1\n"
with mock.patch("subprocess.run", return_value=fake_result) as mock_run:
ip = _get_host_ip()
assert ip == "172.17.0.1"
mock_run.assert_called_once()
args = mock_run.call_args[0][0]
assert "docker" in args
assert "network" in args
assert "inspect" in args
assert "bridge" in args
def test_get_host_ip_falls_back_on_failure():
"""When docker network inspect fails, 'host-gateway' is returned as a fallback."""
fake_result = mock.Mock()
fake_result.returncode = 1
fake_result.stdout = ""
with mock.patch("subprocess.run", return_value=fake_result):
ip = _get_host_ip()
assert ip == "host-gateway"
def test_get_host_ip_falls_back_on_empty_output():
"""When docker network inspect returns empty output, 'host-gateway' is returned."""
fake_result = mock.Mock()
fake_result.returncode = 0
fake_result.stdout = " "
with mock.patch("subprocess.run", return_value=fake_result):
ip = _get_host_ip()
assert ip == "host-gateway"