e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
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
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
# Copyright 2026 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import socket
|
|
from typing import Dict, Optional
|
|
|
|
from fastapi import HTTPException, status
|
|
|
|
from opensandbox_server.services.constants import SandboxErrorCodes
|
|
|
|
DOCKER_PUBLISH_HOST = "0.0.0.0"
|
|
# The probe is a short-lived availability check and must match Docker's
|
|
# publish scope; probing only localhost can miss ports bound on other host
|
|
# interfaces that Docker would later fail to publish.
|
|
PORT_PROBE_HOST = DOCKER_PUBLISH_HOST
|
|
|
|
|
|
def normalize_container_port_spec(port_spec: str) -> str:
|
|
token = str(port_spec).strip()
|
|
if token.endswith("/tcp"):
|
|
return token[:-4]
|
|
return token
|
|
|
|
|
|
def normalize_port_bindings(
|
|
port_bindings: dict[str, tuple[str, int]],
|
|
) -> dict[str, tuple[str, int]]:
|
|
"""
|
|
Normalize binding keys to docker-py canonical forms.
|
|
|
|
Docker port bindings accept "port" for tcp and "port/udp" for udp.
|
|
"""
|
|
normalized: dict[str, tuple[str, int]] = {}
|
|
for container_port, binding in port_bindings.items():
|
|
normalized_key = normalize_container_port_spec(container_port)
|
|
normalized[normalized_key] = binding
|
|
return normalized
|
|
|
|
|
|
def allocate_host_port(
|
|
min_port: int = 40000,
|
|
max_port: int = 60000,
|
|
attempts: int = 50,
|
|
) -> Optional[int]:
|
|
"""Find an available TCP port on the host within the given range."""
|
|
for _ in range(attempts):
|
|
port = random.randint(min_port, max_port)
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
try:
|
|
# This does not listen for or accept connections; it mirrors the
|
|
# later Docker publish binding to catch host-wide port conflicts.
|
|
# codeql[py/bind-socket-all-network-interfaces]
|
|
sock.bind((PORT_PROBE_HOST, port))
|
|
except OSError:
|
|
continue
|
|
return port
|
|
return None
|
|
|
|
|
|
def allocate_port_bindings(
|
|
container_ports: list[str],
|
|
min_port: int = 40000,
|
|
max_port: int = 60000,
|
|
) -> Dict[str, tuple[str, int]]:
|
|
"""Allocate distinct random host ports for each container port spec."""
|
|
allocated_ports: set[int] = set()
|
|
bindings: Dict[str, tuple[str, int]] = {}
|
|
for container_port in container_ports:
|
|
while True:
|
|
host_port = allocate_host_port(min_port=min_port, max_port=max_port)
|
|
if host_port is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail={
|
|
"code": SandboxErrorCodes.CONTAINER_START_FAILED,
|
|
"message": "Failed to allocate host ports for sandbox container.",
|
|
},
|
|
)
|
|
if host_port not in allocated_ports:
|
|
allocated_ports.add(host_port)
|
|
bindings[container_port] = (DOCKER_PUBLISH_HOST, host_port)
|
|
break
|
|
return bindings
|