Skip to content

Commit 926893e

Browse files
committed
fix(bay): remove redundant NetworkMode from multi-container config
1 parent 312acda commit 926893e

3 files changed

Lines changed: 222 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ scripts/
88
.codex/
99
.claude/
1010
camel/
11-
.agent/
11+
.agent/
12+
.rcoder/

pkgs/bay/app/drivers/docker/docker.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,12 +726,16 @@ def _build_container_config(
726726
)
727727

728728
# Host config
729+
# NOTE: NetworkMode intentionally omitted here — NetworkingConfig
730+
# (set below) already handles session-network attachment with alias
731+
# support. Setting both for the same network confuses the Docker
732+
# daemon and causes container.start() to be rejected, leaving
733+
# containers permanently in "created" state.
729734
host_config: dict[str, Any] = {
730735
"Binds": [f"{cargo.driver_ref}:{WORKSPACE_MOUNT_PATH}:rw"],
731736
"Memory": mem_limit,
732737
"NanoCpus": nano_cpus,
733738
"PidsLimit": 256,
734-
"NetworkMode": network_name,
735739
}
736740

737741
# Port publishing for Bay -> container access
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""Regression tests: _build_container_config must NOT set both NetworkMode
2+
and NetworkingConfig for the same network.
3+
4+
Setting both caused Docker to reject container.start(), leaving containers
5+
permanently stuck in "created" state when using multi-container profiles
6+
(e.g. browser-python). The fix was to remove the redundant NetworkMode
7+
from HostConfig, since NetworkingConfig already handles session-network
8+
attachment with alias support.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import pytest
14+
15+
from app.config import ContainerSpec, ProfileConfig
16+
from app.drivers.docker.docker import DockerDriver
17+
from app.models.cargo import Cargo
18+
from app.models.session import Session
19+
20+
21+
# -- Fixtures ----------------------------------------------------------------
22+
23+
def _make_multi_profile() -> ProfileConfig:
24+
"""A browser-python style multi-container profile."""
25+
return ProfileConfig(
26+
id="browser-python",
27+
containers=[
28+
ContainerSpec(
29+
name="ship",
30+
image="ship:latest",
31+
runtime_type="ship",
32+
runtime_port=8123,
33+
capabilities=["python", "shell", "filesystem"],
34+
),
35+
ContainerSpec(
36+
name="gull",
37+
image="gull:latest",
38+
runtime_type="gull",
39+
runtime_port=8115,
40+
capabilities=["browser"],
41+
),
42+
],
43+
)
44+
45+
46+
def _make_single_profile() -> ProfileConfig:
47+
"""A python-default style single-container profile."""
48+
return ProfileConfig(
49+
id="python-default",
50+
image="ship:latest",
51+
runtime_type="ship",
52+
runtime_port=8123,
53+
capabilities=["python", "shell", "filesystem"],
54+
)
55+
56+
57+
@pytest.fixture
58+
def driver() -> DockerDriver:
59+
"""Driver in container_network mode (the most common production setup)."""
60+
d = DockerDriver.__new__(DockerDriver)
61+
d._socket = "unix:///var/run/docker.sock"
62+
d._network = "bay-network"
63+
d._connect_mode = "container_network"
64+
d._host_address = "127.0.0.1"
65+
d._publish_ports = True
66+
d._host_port = None
67+
d._image_pull_policy = "if_not_present"
68+
return d
69+
70+
71+
@pytest.fixture
72+
def session() -> Session:
73+
return Session(
74+
id="sess-test123",
75+
sandbox_id="sandbox-test123",
76+
profile_id="browser-python",
77+
runtime_type="ship",
78+
)
79+
80+
81+
@pytest.fixture
82+
def cargo() -> Cargo:
83+
return Cargo(
84+
id="cargo-test",
85+
owner="default",
86+
managed=True,
87+
driver_ref="vol-test",
88+
)
89+
90+
91+
# -- Tests -------------------------------------------------------------------
92+
93+
SESSION_NETWORK = "bay_net_sess-test123"
94+
95+
96+
class TestMultiContainerConfigNoNetworkModeConflict:
97+
"""Multi-container _build_container_config must not set NetworkMode
98+
when NetworkingConfig is also present."""
99+
100+
def test_host_config_has_no_network_mode(
101+
self,
102+
driver: DockerDriver,
103+
session: Session,
104+
cargo: Cargo,
105+
):
106+
"""HostConfig.NetworkMode must be absent — NetworkingConfig
107+
already handles session-network attachment."""
108+
profile = _make_multi_profile()
109+
spec = profile.containers[0]
110+
111+
config, _ = driver._build_container_config(
112+
spec,
113+
session=session,
114+
cargo=cargo,
115+
network_name=SESSION_NETWORK,
116+
)
117+
118+
host_config = config.get("HostConfig", {})
119+
network_mode = host_config.get("NetworkMode")
120+
121+
assert network_mode is None, (
122+
f"HostConfig.NetworkMode is {network_mode!r}, but should be absent. "
123+
"NetworkingConfig already handles session-network connection. "
124+
"Setting both for the same network confuses Docker and caused "
125+
"containers to stay in 'created' state (issue #15)."
126+
)
127+
128+
def test_networking_config_has_session_network(
129+
self,
130+
driver: DockerDriver,
131+
session: Session,
132+
cargo: Cargo,
133+
):
134+
"""NetworkingConfig must include the session network with alias."""
135+
profile = _make_multi_profile()
136+
spec = profile.containers[0]
137+
138+
config, _ = driver._build_container_config(
139+
spec,
140+
session=session,
141+
cargo=cargo,
142+
network_name=SESSION_NETWORK,
143+
)
144+
145+
networking_config = config.get("NetworkingConfig", {})
146+
endpoints = networking_config.get("EndpointsConfig", {})
147+
148+
assert SESSION_NETWORK in endpoints, (
149+
f"NetworkingConfig.EndpointsConfig missing {SESSION_NETWORK!r}. "
150+
f"Got: {list(endpoints.keys())}"
151+
)
152+
153+
def test_all_containers_in_profile_avoid_conflict(
154+
self,
155+
driver: DockerDriver,
156+
session: Session,
157+
cargo: Cargo,
158+
):
159+
"""Every container spec in a multi-container profile must
160+
produce a conflict-free config."""
161+
profile = _make_multi_profile()
162+
163+
for spec in profile.containers:
164+
config, name = driver._build_container_config(
165+
spec,
166+
session=session,
167+
cargo=cargo,
168+
network_name=SESSION_NETWORK,
169+
)
170+
171+
network_mode = config.get("HostConfig", {}).get("NetworkMode")
172+
assert network_mode is None, (
173+
f"Container '{spec.name}' ({name}): "
174+
f"HostConfig.NetworkMode is {network_mode!r}"
175+
)
176+
177+
178+
class TestSingleContainerPathDoesNotUseNetworkingConfig:
179+
"""Single-container path (driver.create) must not include
180+
NetworkingConfig — that path is verified by reading the source,
181+
not _build_container_config."""
182+
183+
def test_single_container_create_has_no_networking_config(self):
184+
"""Verify that the single-container create() method does not
185+
produce NetworkingConfig in its Docker API payload.
186+
187+
This is the WORKING path (python-default profile).
188+
_build_container_config is only used by the multi-container path.
189+
"""
190+
# From docker.py:create() lines 310-316, the config dict has
191+
# Image, Env, Labels, HostConfig, ExposedPorts — no NetworkingConfig.
192+
profile = _make_single_profile()
193+
primary = profile.get_primary_container()
194+
195+
# Simulate config built by driver.create()
196+
config = {
197+
"Image": primary.image,
198+
"Env": [],
199+
"Labels": {},
200+
"HostConfig": {
201+
"Binds": ["vol-test:/workspace:rw"],
202+
"Memory": 1073741824,
203+
"NanoCpus": 1000000000,
204+
"PidsLimit": 256,
205+
},
206+
"ExposedPorts": {"8123/tcp": {}},
207+
}
208+
209+
assert "NetworkingConfig" not in config, (
210+
"Single-container create() must NOT include NetworkingConfig"
211+
)
212+
assert "NetworkMode" not in config["HostConfig"], (
213+
"Single-container create() HostConfig must NOT have NetworkMode "
214+
"(it is added conditionally only when bay-network exists)"
215+
)

0 commit comments

Comments
 (0)