e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
359 lines
13 KiB
Python
359 lines
13 KiB
Python
# Copyright 2025 Alibaba Group Holding Ltd.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from opensandbox_server.services.constants import (
|
|
OPEN_SANDBOX_EGRESS_AUTH_HEADER,
|
|
SANDBOX_EMBEDDING_PROXY_PORT_LABEL,
|
|
SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY,
|
|
)
|
|
from opensandbox_server.services.docker import DockerSandboxService
|
|
from opensandbox_server.config import AppConfig, RuntimeConfig, DockerConfig, ServerConfig
|
|
|
|
@pytest.fixture
|
|
def mock_docker_service():
|
|
"""Create a DockerSandboxService with mocked docker client."""
|
|
# Setup base config
|
|
config = AppConfig(
|
|
server=ServerConfig(port=8080, host="0.0.0.0"),
|
|
runtime=RuntimeConfig(type="docker", execd_image="test/execd:latest"),
|
|
router=None,
|
|
docker=DockerConfig(network_mode="bridge"),
|
|
)
|
|
|
|
with patch("docker.from_env") as mock_docker:
|
|
mock_client = MagicMock()
|
|
mock_docker.return_value = mock_client
|
|
|
|
# Initialize service
|
|
service = DockerSandboxService(config=config)
|
|
# Inject the mock client directly to ensure we control it
|
|
service.docker_client = mock_client
|
|
|
|
yield service, mock_client
|
|
|
|
def test_get_endpoint_host_mode(mock_docker_service):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "host"
|
|
service.network_mode = "host"
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {"State": {"Running": True}}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="10.0.0.1"):
|
|
endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=False)
|
|
assert endpoint.endpoint == "10.0.0.1:8080"
|
|
|
|
endpoint_internal = service.get_endpoint("sbx-123", 8080, resolve_internal=True)
|
|
assert endpoint_internal.endpoint == "127.0.0.1:8080"
|
|
|
|
|
|
def test_get_endpoint_bridge_http_port(mock_docker_service):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
"opensandbox.io/embedding-proxy-port": "50002",
|
|
"opensandbox.io/http-port": "50001",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": "172.17.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
|
|
endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=False)
|
|
|
|
assert endpoint.endpoint == "192.168.1.100:50001"
|
|
|
|
|
|
def test_get_endpoint_bridge_other_port_via_execd(mock_docker_service):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
"opensandbox.io/embedding-proxy-port": "50002",
|
|
"opensandbox.io/http-port": "50001",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": "172.17.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
|
|
endpoint = service.get_endpoint("sbx-123", 6000, resolve_internal=False)
|
|
|
|
assert endpoint.endpoint == "192.168.1.100:50002/proxy/6000"
|
|
|
|
|
|
def test_get_endpoint_bridge_egress_port_includes_auth_header(mock_docker_service):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
"opensandbox.io/embedding-proxy-port": "50002",
|
|
"opensandbox.io/http-port": "50001",
|
|
"opensandbox.io/egress-auth-token": "egress-token",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": "172.17.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
|
|
endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=False)
|
|
|
|
assert endpoint.endpoint == "192.168.1.100:50002/proxy/18080"
|
|
assert endpoint.headers == {OPEN_SANDBOX_EGRESS_AUTH_HEADER: "egress-token"}
|
|
|
|
|
|
def test_get_endpoint_bridge_non_egress_port_does_not_include_auth_header(
|
|
mock_docker_service,
|
|
):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
|
|
SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": "172.17.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"):
|
|
endpoint = service.get_endpoint("sbx-123", 44772, resolve_internal=False)
|
|
|
|
assert endpoint.endpoint == "192.168.1.100:50002/proxy/44772"
|
|
assert endpoint.headers is None
|
|
|
|
def test_get_endpoint_bridge_internal_resolution(mock_docker_service):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"NetworkSettings": {"IPAddress": "10.0.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=True)
|
|
assert endpoint.endpoint == "10.0.0.5:8080"
|
|
|
|
|
|
def test_get_endpoint_bridge_internal_resolution_with_egress_sidecar_falls_back_to_host_mapped_endpoint(
|
|
mock_docker_service,
|
|
):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
|
|
SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": ""},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=True)
|
|
|
|
assert endpoint.endpoint == "127.0.0.1:50002/proxy/18080"
|
|
assert endpoint.headers is None
|
|
|
|
|
|
def test_get_endpoint_bridge_internal_resolution_with_egress_sidecar_ignores_container_ip(
|
|
mock_docker_service,
|
|
):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
|
|
SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": "10.0.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=True)
|
|
|
|
assert endpoint.endpoint == "127.0.0.1:50002/proxy/18080"
|
|
assert endpoint.headers is None
|
|
|
|
|
|
def test_get_endpoint_bridge_internal_resolution_with_egress_sidecar_uses_proxy_host_not_eip(
|
|
mock_docker_service,
|
|
):
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.server.host = "0.0.0.0"
|
|
service.app_config.server.eip = "203.0.113.10"
|
|
service.app_config.docker.network_mode = "bridge"
|
|
service.network_mode = "bridge"
|
|
|
|
labels = {
|
|
SANDBOX_EMBEDDING_PROXY_PORT_LABEL: "50002",
|
|
SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": ""},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
endpoint = service.get_endpoint("sbx-123", 18080, resolve_internal=True)
|
|
|
|
assert endpoint.endpoint == "127.0.0.1:50002/proxy/18080"
|
|
assert endpoint.headers is None
|
|
|
|
|
|
def test_get_endpoint_bridge_uses_docker_host_ip_when_server_in_container():
|
|
"""When server runs in container (host=0.0.0.0), endpoint uses [docker].host_ip."""
|
|
config = AppConfig(
|
|
server=ServerConfig(port=8080, host="0.0.0.0"),
|
|
runtime=RuntimeConfig(type="docker", execd_image="test/execd:latest"),
|
|
router=None,
|
|
docker=DockerConfig(network_mode="bridge", host_ip="10.57.1.91"),
|
|
)
|
|
with patch("docker.from_env") as mock_docker:
|
|
mock_client = MagicMock()
|
|
mock_docker.return_value = mock_client
|
|
service = DockerSandboxService(config=config)
|
|
service.docker_client = mock_client
|
|
|
|
labels = {
|
|
"opensandbox.io/embedding-proxy-port": "40109",
|
|
"opensandbox.io/http-port": "50001",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {"IPAddress": "172.17.0.5"},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.docker.networking._running_inside_docker_container", return_value=True):
|
|
endpoint = service.get_endpoint("sbx-123", 44772, resolve_internal=False)
|
|
|
|
assert endpoint.endpoint == "10.57.1.91:40109/proxy/44772"
|
|
|
|
|
|
def test_get_endpoint_user_defined_network_external(mock_docker_service):
|
|
"""External endpoint for a user-defined network uses host port bindings, same as bridge."""
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "my-app-net"
|
|
service.network_mode = "my-app-net"
|
|
|
|
labels = {
|
|
"opensandbox.io/embedding-proxy-port": "51000",
|
|
"opensandbox.io/http-port": "51001",
|
|
}
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"Config": {"Labels": labels},
|
|
"NetworkSettings": {
|
|
"IPAddress": "",
|
|
"Networks": {"my-app-net": {"IPAddress": "192.168.100.5"}},
|
|
},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
with patch("opensandbox_server.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="10.0.1.1"):
|
|
ep_http = service.get_endpoint("sbx-123", 8080, resolve_internal=False)
|
|
ep_proxy = service.get_endpoint("sbx-123", 5000, resolve_internal=False)
|
|
|
|
assert ep_http.endpoint == "10.0.1.1:51001"
|
|
assert ep_proxy.endpoint == "10.0.1.1:51000/proxy/5000"
|
|
|
|
|
|
def test_get_endpoint_user_defined_network_internal_prefers_configured_network(mock_docker_service):
|
|
"""resolve_internal=True on a user-defined network returns the IP from that specific network."""
|
|
service, mock_client = mock_docker_service
|
|
service.app_config.docker.network_mode = "my-app-net"
|
|
service.network_mode = "my-app-net"
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"State": {"Running": True},
|
|
"NetworkSettings": {
|
|
# top-level IPAddress is empty for user-defined networks
|
|
"IPAddress": "",
|
|
"Networks": {
|
|
"bridge": {"IPAddress": "172.17.0.3"},
|
|
"my-app-net": {"IPAddress": "192.168.100.5"},
|
|
},
|
|
},
|
|
}
|
|
mock_client.containers.list.return_value = [mock_container]
|
|
|
|
endpoint = service.get_endpoint("sbx-123", 8080, resolve_internal=True)
|
|
|
|
# Must use the IP from the configured network, not the default bridge entry
|
|
assert endpoint.endpoint == "192.168.100.5:8080"
|
|
|
|
|
|
def test_extract_bridge_ip_falls_back_when_named_network_ip_missing(mock_docker_service):
|
|
"""_extract_bridge_ip falls back to any available network IP when the named entry is empty."""
|
|
service, _ = mock_docker_service
|
|
service.network_mode = "my-app-net"
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.attrs = {
|
|
"NetworkSettings": {
|
|
"IPAddress": "",
|
|
"Networks": {
|
|
"my-app-net": {"IPAddress": ""}, # empty — simulate container still attaching
|
|
"bridge": {"IPAddress": "172.17.0.9"},
|
|
},
|
|
},
|
|
}
|
|
|
|
ip = service._extract_bridge_ip(mock_container)
|
|
assert ip == "172.17.0.9"
|