Skip to content
Open
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
33 changes: 32 additions & 1 deletion cli/planoai/docker_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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",
Expand All @@ -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,
]

Expand Down
46 changes: 46 additions & 0 deletions cli/test/test_docker_cli.py
Original file line number Diff line number Diff line change
@@ -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"