/ server / tests / test_docker_endpoint.py
test_docker_endpoint.py
  1  # Copyright 2025 Alibaba Group Holding Ltd.
  2  #
  3  # Licensed under the Apache License, Version 2.0 (the "License");
  4  # you may not use this file except in compliance with the License.
  5  # You may obtain a copy of the License at
  6  #
  7  #     http://www.apache.org/licenses/LICENSE-2.0
  8  #
  9  # Unless required by applicable law or agreed to in writing, software
 10  # distributed under the License is distributed on an "AS IS" BASIS,
 11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  # See the License for the specific language governing permissions and
 13  # limitations under the License.
 14  
 15  import pytest
 16  from unittest.mock import MagicMock, patch
 17  
 18  from opensandbox_server.services.constants import (
 19      OPEN_SANDBOX_EGRESS_AUTH_HEADER,
 20      SANDBOX_EMBEDDING_PROXY_PORT_LABEL,
 21      SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY,
 22  )
 23  from opensandbox_server.services.docker import DockerSandboxService
 24  from opensandbox_server.config import AppConfig, RuntimeConfig, DockerConfig, ServerConfig
 25  
 26  @pytest.fixture
 27  def mock_docker_service():
 28      """Create a DockerSandboxService with mocked docker client."""
 29      # Setup base config
 30      config = AppConfig(
 31          server=ServerConfig(port=8080, host="0.0.0.0"),
 32          runtime=RuntimeConfig(type="docker", execd_image="test/execd:latest"),
 33          router=None,
 34          docker=DockerConfig(network_mode="bridge"),
 35      )
 36  
 37      with patch("docker.from_env") as mock_docker:
 38          mock_client = MagicMock()
 39          mock_docker.return_value = mock_client
 40  
 41          # Initialize service
 42          service = DockerSandboxService(config=config)
 43          # Inject the mock client directly to ensure we control it
 44          service.docker_client = mock_client
 45  
 46          yield service, mock_client
 47  
 48  def test_get_endpoint_host_mode(mock_docker_service):
 49      service, mock_client = mock_docker_service
 50      service.app_config.docker.network_mode = "host"
 51      service.network_mode = "host"
 52  
 53      mock_container = MagicMock()
 54      mock_container.attrs = {"State": {"Running": True}}
 55      mock_client.containers.list.return_value = [mock_container]
 56  
 57      with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="10.0.0.1"):
 58          endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=False)
 59          assert endpoint.endpoint == "10.0.0.1:8080"
 60  
 61      endpoint_internal = service.get_endpoint("sbx-123", 8080, resolve_internal=True)
 62      assert endpoint_internal.endpoint == "127.0.0.1:8080"
 63  
 64  
 65  def test_get_endpoint_bridge_http_port(mock_docker_service):
 66      service, mock_client = mock_docker_service
 67      service.app_config.docker.network_mode = "bridge"
 68      service.network_mode = "bridge"
 69  
 70      labels = {
 71          "opensandbox.io/embedding-proxy-port": "50002",
 72          "opensandbox.io/http-port": "50001",
 73      }
 74      mock_container = MagicMock()
 75      mock_container.attrs = {
 76          "State": {"Running": True},
 77          "Config": {"Labels": labels},
 78          "NetworkSettings": {"IPAddress": "172.17.0.5"},
 79      }
 80      mock_client.containers.list.return_value = [mock_container]
 81  
 82      with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
 83          endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=False)
 84  
 85      assert endpoint.endpoint == "192.168.1.100:50001"
 86  
 87  
 88  def test_get_endpoint_bridge_other_port_via_execd(mock_docker_service):
 89      service, mock_client = mock_docker_service
 90      service.app_config.docker.network_mode = "bridge"
 91      service.network_mode = "bridge"
 92  
 93      labels = {
 94          "opensandbox.io/embedding-proxy-port": "50002",
 95          "opensandbox.io/http-port": "50001",
 96      }
 97      mock_container = MagicMock()
 98      mock_container.attrs = {
 99          "State": {"Running": True},
100          "Config": {"Labels": labels},
101          "NetworkSettings": {"IPAddress": "172.17.0.5"},
102      }
103      mock_client.containers.list.return_value = [mock_container]
104  
105      with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
106          endpoint = service.get_endpoint("sbx-123", 6000, resolve_internal=False)
107  
108      assert endpoint.endpoint == "192.168.1.100:50002/proxy/6000"
109  
110  
111  def test_get_endpoint_bridge_egress_port_includes_auth_header(mock_docker_service):
112      service, mock_client = mock_docker_service
113      service.app_config.docker.network_mode = "bridge"
114      service.network_mode = "bridge"
115  
116      labels = {
117          "opensandbox.io/embedding-proxy-port": "50002",
118          "opensandbox.io/http-port": "50001",
119          "opensandbox.io/egress-auth-token": "egress-token",
120      }
121      mock_container = MagicMock()
122      mock_container.attrs = {
123          "State": {"Running": True},
124          "Config": {"Labels": labels},
125          "NetworkSettings": {"IPAddress": "172.17.0.5"},
126      }
127      mock_client.containers.list.return_value = [mock_container]
128  
129      with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
130          endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=False)
131  
132      assert endpoint.endpoint == "192.168.1.100:50002/proxy/18080"
133      assert endpoint.headers == {OPEN_SANDBOX_EGRESS_AUTH_HEADER: "egress-token"}
134  
135  
136  def test_get_endpoint_bridge_non_egress_port_still_includes_instance_auth_header(
137      mock_docker_service,
138  ):
139      service, mock_client = mock_docker_service
140      service.app_config.docker.network_mode = "bridge"
141      service.network_mode = "bridge"
142  
143      labels = {
144          SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
145          SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
146      }
147      mock_container = MagicMock()
148      mock_container.attrs = {
149          "State": {"Running": True},
150          "Config": {"Labels": labels},
151          "NetworkSettings": {"IPAddress": "172.17.0.5"},
152      }
153      mock_client.containers.list.return_value = [mock_container]
154  
155      with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
156          endpoint = service.get_endpoint("sbx-123", 44772, resolve_internal=False)
157  
158      assert endpoint.endpoint == "192.168.1.100:50002/proxy/44772"
159      assert endpoint.headers == {OPEN_SANDBOX_EGRESS_AUTH_HEADER: "egress-token"}
160  
161  def test_get_endpoint_bridge_internal_resolution(mock_docker_service):
162      service, mock_client = mock_docker_service
163      service.app_config.docker.network_mode = "bridge"
164      service.network_mode = "bridge"
165  
166      mock_container = MagicMock()
167      mock_container.attrs = {
168          "State": {"Running": True},
169          "NetworkSettings": {"IPAddress": "10.0.0.5"},
170      }
171      mock_client.containers.list.return_value = [mock_container]
172  
173      endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=True)
174      assert endpoint.endpoint == "10.0.0.5:8080"
175  
176  
177  def test_get_endpoint_bridge_internal_resolution_with_egress_sidecar_falls_back_to_host_mapped_endpoint(
178      mock_docker_service,
179  ):
180      service, mock_client = mock_docker_service
181      service.app_config.docker.network_mode = "bridge"
182      service.network_mode = "bridge"
183  
184      labels = {
185          SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
186          SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
187      }
188      mock_container = MagicMock()
189      mock_container.attrs = {
190          "State": {"Running": True},
191          "Config": {"Labels": labels},
192          "NetworkSettings": {"IPAddress": ""},
193      }
194      mock_client.containers.list.return_value = [mock_container]
195  
196      endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=True)
197  
198      assert endpoint.endpoint == "127.0.0.1:50002/proxy/18080"
199      assert endpoint.headers == {OPEN_SANDBOX_EGRESS_AUTH_HEADER: "egress-token"}
200  
201  
202  def test_get_endpoint_bridge_internal_resolution_with_egress_sidecar_ignores_container_ip(
203      mock_docker_service,
204  ):
205      service, mock_client = mock_docker_service
206      service.app_config.docker.network_mode = "bridge"
207      service.network_mode = "bridge"
208  
209      labels = {
210          SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
211          SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
212      }
213      mock_container = MagicMock()
214      mock_container.attrs = {
215          "State": {"Running": True},
216          "Config": {"Labels": labels},
217          "NetworkSettings": {"IPAddress": "10.0.0.5"},
218      }
219      mock_client.containers.list.return_value = [mock_container]
220  
221      endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=True)
222  
223      assert endpoint.endpoint == "127.0.0.1:50002/proxy/18080"
224      assert endpoint.headers == {OPEN_SANDBOX_EGRESS_AUTH_HEADER: "egress-token"}
225  
226  
227  def test_get_endpoint_bridge_internal_resolution_with_egress_sidecar_uses_proxy_host_not_eip(
228      mock_docker_service,
229  ):
230      service, mock_client = mock_docker_service
231      service.app_config.server.host = "0.0.0.0"
232      service.app_config.server.eip = "203.0.113.10"
233      service.app_config.docker.network_mode = "bridge"
234      service.network_mode = "bridge"
235  
236      labels = {
237          SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
238          SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
239      }
240      mock_container = MagicMock()
241      mock_container.attrs = {
242          "State": {"Running": True},
243          "Config": {"Labels": labels},
244          "NetworkSettings": {"IPAddress": ""},
245      }
246      mock_client.containers.list.return_value = [mock_container]
247  
248      endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=True)
249  
250      assert endpoint.endpoint == "127.0.0.1:50002/proxy/18080"
251      assert endpoint.headers == {OPEN_SANDBOX_EGRESS_AUTH_HEADER: "egress-token"}
252  
253  
254  def test_get_endpoint_bridge_uses_docker_host_ip_when_server_in_container():
255      """When server runs in container (host=0.0.0.0), endpoint uses [docker].host_ip."""
256      config = AppConfig(
257          server=ServerConfig(port=8080, host="0.0.0.0"),
258          runtime=RuntimeConfig(type="docker", execd_image="test/execd:latest"),
259          router=None,
260          docker=DockerConfig(network_mode="bridge", host_ip="10.57.1.91"),
261      )
262      with patch("docker.from_env") as mock_docker:
263          mock_client = MagicMock()
264          mock_docker.return_value = mock_client
265          service = DockerSandboxService(config=config)
266          service.docker_client = mock_client
267  
268      labels = {
269          "opensandbox.io/embedding-proxy-port": "40109",
270          "opensandbox.io/http-port": "50001",
271      }
272      mock_container = MagicMock()
273      mock_container.attrs = {
274          "State": {"Running": True},
275          "Config": {"Labels": labels},
276          "NetworkSettings": {"IPAddress": "172.17.0.5"},
277      }
278      mock_client.containers.list.return_value = [mock_container]
279  
280      with patch("opensandbox_server.services.docker._running_inside_docker_container", return_value=True):
281          endpoint = service.get_endpoint("sbx-123", 44772, resolve_internal=False)
282  
283      assert endpoint.endpoint == "10.57.1.91:40109/proxy/44772"
284  
285  
286  def test_get_endpoint_user_defined_network_external(mock_docker_service):
287      """External endpoint for a user-defined network uses host port bindings, same as bridge."""
288      service, mock_client = mock_docker_service
289      service.app_config.docker.network_mode = "my-app-net"
290      service.network_mode = "my-app-net"
291  
292      labels = {
293          "opensandbox.io/embedding-proxy-port": "51000",
294          "opensandbox.io/http-port": "51001",
295      }
296      mock_container = MagicMock()
297      mock_container.attrs = {
298          "State": {"Running": True},
299          "Config": {"Labels": labels},
300          "NetworkSettings": {
301              "IPAddress": "",
302              "Networks": {"my-app-net": {"IPAddress": "192.168.100.5"}},
303          },
304      }
305      mock_client.containers.list.return_value = [mock_container]
306  
307      with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="10.0.1.1"):
308          ep_http = service.get_endpoint("sbx-123", 8080, resolve_internal=False)
309          ep_proxy = service.get_endpoint("sbx-123", 5000, resolve_internal=False)
310  
311      assert ep_http.endpoint == "10.0.1.1:51001"
312      assert ep_proxy.endpoint == "10.0.1.1:51000/proxy/5000"
313  
314  
315  def test_get_endpoint_user_defined_network_internal_prefers_configured_network(mock_docker_service):
316      """resolve_internal=True on a user-defined network returns the IP from that specific network."""
317      service, mock_client = mock_docker_service
318      service.app_config.docker.network_mode = "my-app-net"
319      service.network_mode = "my-app-net"
320  
321      mock_container = MagicMock()
322      mock_container.attrs = {
323          "State": {"Running": True},
324          "NetworkSettings": {
325              # top-level IPAddress is empty for user-defined networks
326              "IPAddress": "",
327              "Networks": {
328                  "bridge": {"IPAddress": "172.17.0.3"},
329                  "my-app-net": {"IPAddress": "192.168.100.5"},
330              },
331          },
332      }
333      mock_client.containers.list.return_value = [mock_container]
334  
335      endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=True)
336  
337      # Must use the IP from the configured network, not the default bridge entry
338      assert endpoint.endpoint == "192.168.100.5:8080"
339  
340  
341  def test_extract_bridge_ip_falls_back_when_named_network_ip_missing(mock_docker_service):
342      """_extract_bridge_ip falls back to any available network IP when the named entry is empty."""
343      service, _ = mock_docker_service
344      service.network_mode = "my-app-net"
345  
346      mock_container = MagicMock()
347      mock_container.attrs = {
348          "NetworkSettings": {
349              "IPAddress": "",
350              "Networks": {
351                  "my-app-net": {"IPAddress": ""},   # empty — simulate container still attaching
352                  "bridge": {"IPAddress": "172.17.0.9"},
353              },
354          },
355      }
356  
357      ip = service._extract_bridge_ip(mock_container)
358      assert ip == "172.17.0.9"