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"