diff --git a/.github/workflows/real-e2e.yml b/.github/workflows/real-e2e.yml index 5b958e76c..918582b14 100644 --- a/.github/workflows/real-e2e.yml +++ b/.github/workflows/real-e2e.yml @@ -7,7 +7,7 @@ on: pull_request: branches: [ main ] paths: - - 'server/src/**' + - 'server/opensandbox_server/**' - 'components/execd/**' - 'components/egress/**' - 'sdks/code-interpreter/**' @@ -92,7 +92,7 @@ jobs: run: | docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true - pkill -f "python -m src.main" || true + pkill -f "python -m opensandbox_server.main" || true java-e2e: name: Java E2E (docker bridge) @@ -184,7 +184,7 @@ jobs: run: | docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true - pkill -f "python -m src.main" || true + pkill -f "python -m opensandbox_server.main" || true javascript-e2e: name: JavaScript E2E (docker bridge) @@ -277,7 +277,7 @@ jobs: run: | docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true - pkill -f "python -m src.main" || true + pkill -f "python -m opensandbox_server.main" || true csharp-e2e: name: C# E2E (docker bridge) @@ -359,4 +359,4 @@ jobs: run: | docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true - pkill -f "python -m src.main" || true + pkill -f "python -m opensandbox_server.main" || true diff --git a/.github/workflows/server-test.yml b/.github/workflows/server-test.yml index 5e4604c85..86fc6a838 100644 --- a/.github/workflows/server-test.yml +++ b/.github/workflows/server-test.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [ main ] paths: - - 'server/src/**' + - 'server/opensandbox_server/**' - 'server/tests/**' permissions: @@ -91,7 +91,7 @@ jobs: EOF # Start server in background - uv run python -m src.main > app.log 2>&1 & + uv run python -m opensandbox_server.main > app.log 2>&1 & # Wait for server to start sleep 10 diff --git a/AGENTS.md b/AGENTS.md index a87877af7..e1e7e9016 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,8 @@ ## Build, Test, and Development Commands - Server (Python): - `cd server && uv sync` installs deps. - - `cp server/example.config.toml ~/.sandbox.toml` sets local config. - - `cd server && uv run python -m src.main` runs the API server. + - `cp server/opensandbox_server/examples/example.config.toml ~/.sandbox.toml` sets local config. + - `cd server && uv run python -m opensandbox_server.main` runs the API server. - execd (Go): - `cd components/execd && go build -o bin/execd .` builds the daemon. - `cd components/execd && make fmt` formats Go sources. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55812dfe4..581d207ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,15 +73,15 @@ cd server # Install dependencies uv sync -# Copy example configuration -cp example.config.toml ~/.sandbox.toml +# Copy example configuration from the source tree +cp server/opensandbox_server/examples/example.config.toml ~/.sandbox.toml # Edit configuration for development # Set log_level = "DEBUG" and api_key nano ~/.sandbox.toml # Run server -uv run python -m src.main +uv run python -m opensandbox_server.main ``` See [server/DEVELOPMENT.md](server/DEVELOPMENT.md) for detailed server development guide. diff --git a/README.md b/README.md index e8db3d0ac..de16256ed 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,8 @@ opensandbox-server init-config ~/.sandbox.toml --example docker > git clone https://github.com/alibaba/OpenSandbox.git > cd OpenSandbox/server > uv sync -> cp example.config.toml ~/.sandbox.toml # Copy configuration file -> uv run python -m src.main # Start the service +> cp opensandbox_server/examples/example.config.toml ~/.sandbox.toml # Copy configuration file from the source tree +> uv run python -m opensandbox_server.main # Start the service > ``` #### 2. Start the Sandbox Server diff --git a/docs/README_zh.md b/docs/README_zh.md index 4642159e8..2f9f8bb5e 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -73,8 +73,8 @@ opensandbox-server init-config ~/.sandbox.toml --example docker-zh > git clone https://github.com/alibaba/OpenSandbox.git > cd OpenSandbox/server > uv sync -> cp example.config.toml ~/.sandbox.toml # Copy configuration file -> uv run python -m src.main # Start the service +> cp opensandbox_server/examples/example.config.zh.toml ~/.sandbox.toml # 从源码目录复制配置文件 +> uv run python -m opensandbox_server.main # Start the service > ``` #### 2. 启动沙箱 Server diff --git a/docs/manual-cleanup-refactor-guide.md b/docs/manual-cleanup-refactor-guide.md index 7d3928f37..17b302bbe 100644 --- a/docs/manual-cleanup-refactor-guide.md +++ b/docs/manual-cleanup-refactor-guide.md @@ -85,7 +85,7 @@ TTL is currently mandatory. Relevant files: -- `server/src/api/schema.py` +- `server/opensandbox_server/api/schema.py` - `specs/sandbox-lifecycle.yml` Current constraints: @@ -99,7 +99,7 @@ Current constraints: Relevant file: -- `server/src/services/docker.py` +- `server/opensandbox_server/services/docker.py` Current behavior: @@ -113,9 +113,9 @@ Current behavior: Relevant files: -- `server/src/services/k8s/kubernetes_service.py` -- `server/src/services/k8s/batchsandbox_provider.py` -- `server/src/services/k8s/agent_sandbox_provider.py` +- `server/opensandbox_server/services/k8s/kubernetes_service.py` +- `server/opensandbox_server/services/k8s/batchsandbox_provider.py` +- `server/opensandbox_server/services/k8s/agent_sandbox_provider.py` Current behavior: @@ -197,7 +197,7 @@ Recommended error message: Files to update: -- `server/src/api/schema.py` +- `server/opensandbox_server/api/schema.py` - `specs/sandbox-lifecycle.yml` Required changes: @@ -217,7 +217,7 @@ Recommended validation rule: File to update: -- `server/src/services/docker.py` +- `server/opensandbox_server/services/docker.py` ### Target behavior @@ -302,10 +302,10 @@ Target logic: Files to update: -- `server/src/services/k8s/kubernetes_service.py` -- `server/src/services/k8s/workload_provider.py` -- `server/src/services/k8s/batchsandbox_provider.py` -- `server/src/services/k8s/agent_sandbox_provider.py` +- `server/opensandbox_server/services/k8s/kubernetes_service.py` +- `server/opensandbox_server/services/k8s/workload_provider.py` +- `server/opensandbox_server/services/k8s/batchsandbox_provider.py` +- `server/opensandbox_server/services/k8s/agent_sandbox_provider.py` ### Key risk @@ -378,8 +378,8 @@ If not supported by the CRD: Files likely affected: -- `server/src/services/sandbox_service.py` -- `server/src/services/k8s/workload_provider.py` +- `server/opensandbox_server/services/sandbox_service.py` +- `server/opensandbox_server/services/k8s/workload_provider.py` Required updates: @@ -500,7 +500,7 @@ Follow-up checks: ## Suggested Implementation Order -1. Update schema models in `server/src/api/schema.py` +1. Update schema models in `server/opensandbox_server/api/schema.py` 2. Update OpenAPI spec in `specs/sandbox-lifecycle.yml` 3. Refactor Docker runtime to support `expires_at: Optional[datetime]` 4. Add Kubernetes provider capability plumbing diff --git a/examples/host-volume-mount/README.md b/examples/host-volume-mount/README.md index 904708d29..60fbaeb85 100644 --- a/examples/host-volume-mount/README.md +++ b/examples/host-volume-mount/README.md @@ -17,8 +17,8 @@ This example demonstrates how to mount host directories into sandbox containers ```shell git clone git@github.com:alibaba/OpenSandbox.git cd OpenSandbox/server -cp example.config.toml ~/.sandbox.toml -uv sync && uv run python -m src.main +cp opensandbox_server/examples/example.config.toml ~/.sandbox.toml +uv sync && uv run python -m opensandbox_server.main ``` ### 2. Configure Allowed Host Paths diff --git a/examples/host-volume-mount/README_zh.md b/examples/host-volume-mount/README_zh.md index c9b7c2789..86d7bf414 100644 --- a/examples/host-volume-mount/README_zh.md +++ b/examples/host-volume-mount/README_zh.md @@ -17,8 +17,8 @@ ```shell git clone git@github.com:alibaba/OpenSandbox.git cd OpenSandbox/server -cp example.config.zh.toml ~/.sandbox.toml -uv sync && uv run python -m src.main +cp opensandbox_server/examples/example.config.zh.toml ~/.sandbox.toml +uv sync && uv run python -m opensandbox_server.main ``` ### 2. 配置允许的宿主机路径 diff --git a/oseps/0001-fqdn-based-egress-control.md b/oseps/0001-fqdn-based-egress-control.md index 6a10e492b..2c92c1e76 100644 --- a/oseps/0001-fqdn-based-egress-control.md +++ b/oseps/0001-fqdn-based-egress-control.md @@ -1118,9 +1118,9 @@ The key insight is that `CAP_NET_ADMIN` grants permission to modify network conf #### 1. Server (`server/`) -**`server/src/api/schema.py`**: Add `NetworkPolicy` schema classes. +**`server/opensandbox_server/api/schema.py`**: Add `NetworkPolicy` schema classes. -**`server/src/services/docker.py`** (sidecar pattern): +**`server/opensandbox_server/services/docker.py`** (sidecar pattern): - Create an egress sidecar container when `network_policy` is present. - Add `CAP_NET_ADMIN` only to the sidecar. - Set `OPENSANDBOX_EGRESS_TOKEN` env (random per-sandbox) and optionally `OPENSANDBOX_EGRESS_HTTP_ADDR`. @@ -1128,7 +1128,7 @@ The key insight is that `CAP_NET_ADMIN` grants permission to modify network conf - Wait for sidecar `/healthz` 200, then POST `networkPolicy` to `/policy` with header `OPENSANDBOX-EGRESS-AUTH: `. - Reject `--network host` when `network_policy` is set (hostNetwork not supported). -**`server/src/services/k8s/batchsandbox_provider.py`** (Pod pattern): +**`server/opensandbox_server/services/k8s/batchsandbox_provider.py`** (Pod pattern): - Pod spec includes `egress-sidecar` with `capabilities.add: [NET_ADMIN]` and the application container without extra caps. - Sidecar env includes `OPENSANDBOX_EGRESS_TOKEN` (and `OPENSANDBOX_EGRESS_HTTP_ADDR` if non-default); may optionally seed `OPENSANDBOX_EGRESS_RULES`. - Server (inside cluster) waits for `/healthz` on the Pod IP, then POSTs `networkPolicy` to `/policy` with header `OPENSANDBOX-EGRESS-AUTH`. diff --git a/oseps/0004-secure-container-runtime.md b/oseps/0004-secure-container-runtime.md index 3f60456b8..1255783d1 100644 --- a/oseps/0004-secure-container-runtime.md +++ b/oseps/0004-secure-container-runtime.md @@ -559,7 +559,7 @@ def validate_secure_runtime_on_startup(config: AppConfig, docker_client=None, k8 ### Docker Mode Implementation -Changes to `server/src/services/docker.py`. The runtime is read from server config, not from the request: +Changes to `server/opensandbox_server/services/docker.py`. The runtime is read from server config, not from the request: ```python class DockerSandboxService(SandboxService): @@ -585,7 +585,7 @@ Both Kubernetes workload providers inject `runtimeClassName` from server config. #### BatchSandboxProvider -Changes to `server/src/services/k8s/batchsandbox_provider.py`: +Changes to `server/opensandbox_server/services/k8s/batchsandbox_provider.py`: - **CRD**: `sandbox.opensandbox.io/v1alpha1` BatchSandbox - **Pod spec path**: `spec.template.spec` @@ -608,7 +608,7 @@ class BatchSandboxProvider: #### AgentSandboxProvider -Changes to `server/src/services/k8s/agent_sandbox_provider.py`: +Changes to `server/opensandbox_server/services/k8s/agent_sandbox_provider.py`: - **CRD**: `agents.x-k8s.io/v1alpha1` Sandbox - **Pod spec path**: `spec.podTemplate.spec` diff --git a/oseps/0006-developer-console.md b/oseps/0006-developer-console.md index d32f8c05f..da9f85cb4 100644 --- a/oseps/0006-developer-console.md +++ b/oseps/0006-developer-console.md @@ -49,8 +49,8 @@ OIDC JWT validation, PostgreSQL RBAC bindings, and durable audit logs. Today OpenSandbox exposes lifecycle APIs and Swagger docs, but developers/operators still need to manage sandbox resources via APIs. This creates friction for common workflows (search/create/renew/delete), weakens governance in multi-user environments, and raises onboarding cost for teams that are not API-first. -- Server auth today is global API key only (`server/src/middleware/auth.py` with `OPEN-SANDBOX-API-KEY`). -- Lifecycle operations already exist and are stable (`server/src/api/lifecycle.py`, `specs/sandbox-lifecycle.yml`). +- Server auth today is global API key only (`server/opensandbox_server/middleware/auth.py` with `OPEN-SANDBOX-API-KEY`). +- Lifecycle operations already exist and are stable (`server/opensandbox_server/api/lifecycle.py`, `specs/sandbox-lifecycle.yml`). - Filtering by state/metadata already exists (`GET /sandboxes` and `matches_filter`). - Sandbox metadata already maps to labels in Docker/Kubernetes services and is returned in list/get responses. @@ -152,9 +152,9 @@ flowchart LR Quick summary of the relevant server code as it stands today: -- Auth middleware: API key only (`server/src/middleware/auth.py`, header `OPEN-SANDBOX-API-KEY`). -- Lifecycle routes: `server/src/api/lifecycle.py`. -- Service implementations: `server/src/services/docker.py` (Docker), `server/src/services/k8s/kubernetes_service.py` (Kubernetes). +- Auth middleware: API key only (`server/opensandbox_server/middleware/auth.py`, header `OPEN-SANDBOX-API-KEY`). +- Lifecycle routes: `server/opensandbox_server/api/lifecycle.py`. +- Service implementations: `server/opensandbox_server/services/docker.py` (Docker), `server/opensandbox_server/services/k8s/kubernetes_service.py` (Kubernetes). - Filtering: `state` and `metadata` filters in route parsing, `matches_filter` helper. - Metadata: already stored as Docker/Kubernetes labels. @@ -219,7 +219,7 @@ Canonicalization: #### 1. Configuration -Extend `server/src/config.py` with auth/authz sections. +Extend `server/opensandbox_server/config.py` with auth/authz sections. `auth.mode` controls high-level authentication behavior: @@ -274,7 +274,7 @@ jwks_url = "https://www.googleapis.com/oauth2/v3/certs" #### 2. Authentication Middleware -Changes to `server/src/middleware/auth.py`: +Changes to `server/opensandbox_server/middleware/auth.py`: 1. Preserve current API key path exactly. 2. Add user principal extraction path (phase-gated by config). @@ -284,12 +284,12 @@ Changes to `server/src/middleware/auth.py`: #### 3. Authorization Enforcement -New module `server/src/middleware/authorization.py` with a single entry point: +New module `server/opensandbox_server/middleware/authorization.py` with a single entry point: - `authorize_action(principal, action, sandbox=None)`. - Scope checks for owner/team. -Integrate into `server/src/api/lifecycle.py` per route before invoking mutating service operations. +Integrate into `server/opensandbox_server/api/lifecycle.py` per route before invoking mutating service operations. For list operations: Apply server-side scope filter in addition to client-provided filters. diff --git a/oseps/0007-fast-sandbox-runtime-support.md b/oseps/0007-fast-sandbox-runtime-support.md index 6d808a00a..040cdb764 100644 --- a/oseps/0007-fast-sandbox-runtime-support.md +++ b/oseps/0007-fast-sandbox-runtime-support.md @@ -572,7 +572,7 @@ To handle these cases, fast-sandbox provides a **Node Janitor DaemonSet** that r ### Configuration Extension -Add `FastSandboxRuntimeConfig` to `server/src/config.py`: +Add `FastSandboxRuntimeConfig` to `server/opensandbox_server/config.py`: ```python class FastSandboxRuntimeConfig(BaseModel): @@ -627,7 +627,7 @@ execd_port = 44772 ### New Code Structure ``` -server/src/services/k8s/ +server/opensandbox_server/services/k8s/ ├── fastsandbox_provider.py # New: FastSandboxProvider WorkloadProvider implementation ├── fastsandbox_client.py # New: gRPC client wrapper for fast-sandbox Controller ├── provider_factory.py # Modified: Register "fast-sandbox" provider diff --git a/scripts/csharp-e2e.sh b/scripts/csharp-e2e.sh index 02e3b3ed2..adc39f186 100755 --- a/scripts/csharp-e2e.sh +++ b/scripts/csharp-e2e.sh @@ -45,7 +45,7 @@ echo "-------- CSHARP E2E test logs for execd --------" > /tmp/opensandbox-e2e/l # setup server cd server : > server.log -(uv sync && uv run python -m src.main) > server.log 2>&1 & +(uv sync && uv run python -m opensandbox_server.main) > server.log 2>&1 & cd .. # wait for server diff --git a/scripts/java-e2e.sh b/scripts/java-e2e.sh index 245843402..8fb20e509 100644 --- a/scripts/java-e2e.sh +++ b/scripts/java-e2e.sh @@ -45,7 +45,7 @@ echo "-------- JAVA E2E test logs for execd --------" > /tmp/opensandbox-e2e/log # setup server cd server -uv sync && uv run python -m src.main > server.log 2>&1 & +uv sync && uv run python -m opensandbox_server.main > server.log 2>&1 & cd .. # wait for server diff --git a/scripts/javascript-e2e.sh b/scripts/javascript-e2e.sh index 75148f288..dc370f70c 100644 --- a/scripts/javascript-e2e.sh +++ b/scripts/javascript-e2e.sh @@ -45,7 +45,7 @@ echo "-------- JAVASCRIPT E2E test logs for execd --------" > /tmp/opensandbox-e # setup server cd server -uv sync && uv run python -m src.main > server.log 2>&1 & +uv sync && uv run python -m opensandbox_server.main > server.log 2>&1 & cd .. # wait for server @@ -69,4 +69,3 @@ export OPENSANDBOX_TEST_API_KEY="" export OPENSANDBOX_SANDBOX_DEFAULT_IMAGE="opensandbox/code-interpreter:${TAG}" pnpm test:ci - diff --git a/scripts/python-e2e.sh b/scripts/python-e2e.sh index 68eedd9df..6ed660d28 100755 --- a/scripts/python-e2e.sh +++ b/scripts/python-e2e.sh @@ -49,7 +49,7 @@ echo "-------- PYTHON E2E test logs for execd --------" > /tmp/opensandbox-e2e/l # setup server cd server -uv sync && uv run python -m src.main > server.log 2>&1 & +uv sync && uv run python -m opensandbox_server.main > server.log 2>&1 & cd .. # wait for server diff --git a/server/DEVELOPMENT.md b/server/DEVELOPMENT.md index 88f9435ec..a6c43cef0 100644 --- a/server/DEVELOPMENT.md +++ b/server/DEVELOPMENT.md @@ -46,7 +46,7 @@ This guide provides comprehensive information for developers working on OpenSand 4. **Configure Development Environment** ```bash - cp example.config.toml ~/.sandbox.toml + cp opensandbox_server/examples/example.config.toml ~/.sandbox.toml ``` Edit `~/.sandbox.toml` for local development: @@ -67,7 +67,7 @@ This guide provides comprehensive information for developers working on OpenSand 5. **Run Development Server** ```bash - uv run python -m src.main + uv run python -m opensandbox_server.main ``` ### IDE Configuration @@ -84,7 +84,7 @@ Create `.vscode/launch.json`: "name": "Python: FastAPI", "type": "python", "request": "launch", - "module": "src.main", + "module": "opensandbox_server.main", "justMyCode": false, "env": { "SANDBOX_CONFIG_PATH": "${workspaceFolder}/.sandbox.toml" @@ -105,7 +105,7 @@ Create `.vscode/launch.json`: ``` server/ -├── src/ # Source code +├── opensandbox_server/ # Source code │ ├── main.py # FastAPI application entry point │ ├── config.py # Configuration management │ ├── api/ # API layer @@ -204,7 +204,7 @@ uv run pytest uv run pytest tests/test_docker_service.py # With coverage -uv run pytest --cov=src --cov-report=html +uv run pytest --cov=opensandbox_server --cov-report=html ``` ### Writing Tests @@ -212,7 +212,7 @@ uv run pytest --cov=src --cov-report=html Example unit test: ```python -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_create_sandbox_validates_entrypoint(mock_docker): service = DockerSandboxService(config=test_config()) request = CreateSandboxRequest( @@ -231,11 +231,11 @@ def test_create_sandbox_validates_entrypoint(mock_docker): ```bash # Use local Docker export DOCKER_HOST="unix:///var/run/docker.sock" -uv run python -m src.main +uv run python -m opensandbox_server.main # Use remote Docker export DOCKER_HOST="ssh://user@remote-host" -uv run python -m src.main +uv run python -m opensandbox_server.main ``` ### Network Modes @@ -272,7 +272,7 @@ Architecture will include: Follow PEP 8 with Ruff enforcement: ```bash -uv run ruff check src tests +uv run ruff check opensandbox_server tests ``` ### Naming Conventions @@ -320,7 +320,7 @@ logging.getLogger("docker").setLevel(logging.DEBUG) ### Profiling ```bash -python -m cProfile -o profile.stats -m src.main +python -m cProfile -o profile.stats -m opensandbox_server.main ``` ### Optimization Tips diff --git a/server/Dockerfile b/server/Dockerfile index b48869065..6a8667ee8 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -32,7 +32,7 @@ ENV PATH="/root/.local/bin:/root/.cargo/bin:${PATH}" COPY pyproject.toml uv.lock ./ RUN uv sync --frozen --no-dev --no-install-project -COPY src ./src +COPY opensandbox_server ./opensandbox_server COPY LICENSE README.md README_zh.md example.config.toml example.config.zh.toml \ example.config.k8s.toml example.config.k8s.zh.toml example.batchsandbox-template.yaml ./ @@ -51,7 +51,7 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ WORKDIR /app COPY --from=builder /app/.venv /app/.venv -COPY --from=builder /app/src /app/src +COPY --from=builder /app/opensandbox_server /app/opensandbox_server COPY --from=builder /app/example.config.k8s.toml /etc/opensandbox/config.toml COPY --from=builder /app/example.config.k8s.zh.toml /etc/opensandbox/config.zh.toml COPY --from=builder /app/example.batchsandbox-template.yaml /etc/opensandbox/example.batchsandbox-template.yaml diff --git a/server/README.md b/server/README.md index 2637a2f89..df7631871 100644 --- a/server/README.md +++ b/server/README.md @@ -475,10 +475,10 @@ curl -X DELETE \ ### Component responsibilities -- **API Layer** (`src/api/`): HTTP request handling, validation, and response formatting -- **Service Layer** (`src/services/`): Business logic for sandbox lifecycle operations -- **Middleware** (`src/middleware/`): Cross-cutting concerns (authentication, logging) -- **Configuration** (`src/config.py`): Centralized configuration management +- **API Layer** (`opensandbox_server/api/`): HTTP request handling, validation, and response formatting +- **Service Layer** (`opensandbox_server/services/`): Business logic for sandbox lifecycle operations +- **Middleware** (`opensandbox_server/middleware/`): Cross-cutting concerns (authentication, logging) +- **Configuration** (`opensandbox_server/config.py`): Centralized configuration management - **Runtime Implementations**: Platform-specific sandbox orchestration ### Sandbox lifecycle states @@ -623,7 +623,7 @@ uv run pytest **Run with coverage**: ```bash -uv run pytest --cov=src --cov-report=html +uv run pytest --cov=opensandbox_server --cov-report=html ``` **Run specific test**: diff --git a/server/README_zh.md b/server/README_zh.md index a97d0ec5f..efbba6a6c 100644 --- a/server/README_zh.md +++ b/server/README_zh.md @@ -450,10 +450,10 @@ curl -X DELETE \ ### 组件职责 -- **API 层**(`src/api/`):HTTP 请求处理、验证和响应格式化 -- **服务层**(`src/services/`):沙箱生命周期操作的业务逻辑 -- **中间件**(`src/middleware/`):横切关注点(认证、日志) -- **配置**(`src/config.py`):集中式配置管理 +- **API 层**(`opensandbox_server/api/`):HTTP 请求处理、验证和响应格式化 +- **服务层**(`opensandbox_server/services/`):沙箱生命周期操作的业务逻辑 +- **中间件**(`opensandbox_server/middleware/`):横切关注点(认证、日志) +- **配置**(`opensandbox_server/config.py`):集中式配置管理 - **运行时实现**:平台特定的沙箱编排 ### 沙箱生命周期状态 @@ -598,7 +598,7 @@ uv run pytest **带覆盖率运行**: ```bash -uv run pytest --cov=src --cov-report=html +uv run pytest --cov=opensandbox_server --cov-report=html ``` **运行特定测试**: diff --git a/server/RELEASE_NOTES.md b/server/RELEASE_NOTES.md index bbd14cb25..ebf196135 100644 --- a/server/RELEASE_NOTES.md +++ b/server/RELEASE_NOTES.md @@ -315,4 +315,4 @@ Thanks to these contributors ❤️ --- - PyPI: opensandbox-server==0.1.0 - Docker Hub: opensandbox/server:v0.1.0 -- Aliyun Registry: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/server:v0.1.0 \ No newline at end of file +- Aliyun Registry: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/server:v0.1.0 diff --git a/server/example.config.k8s.toml b/server/example.config.k8s.toml index 20ecaa576..b37d543fd 100644 --- a/server/example.config.k8s.toml +++ b/server/example.config.k8s.toml @@ -20,7 +20,7 @@ # Usage: # 1. Copy this file to ~/.sandbox.toml (or set SANDBOX_CONFIG_PATH environment variable) # 2. Update the configuration values according to your environment -# 3. Start the server: uvicorn src.main:app --host 0.0.0.0 --port 8080 +# 3. Start the server: uvicorn opensandbox_server.main:app --host 0.0.0.0 --port 8080 [server] host = "0.0.0.0" diff --git a/server/example.config.k8s.zh.toml b/server/example.config.k8s.zh.toml index 2ad65e350..20915a474 100644 --- a/server/example.config.k8s.zh.toml +++ b/server/example.config.k8s.zh.toml @@ -20,7 +20,7 @@ # Usage: # 1. Copy this file to ~/.sandbox.toml (or set SANDBOX_CONFIG_PATH environment variable) # 2. Update the configuration values according to your environment -# 3. Start the server: uvicorn src.main:app --host 0.0.0.0 --port 8080 +# 3. Start the server: uvicorn opensandbox_server.main:app --host 0.0.0.0 --port 8080 [server] host = "0.0.0.0" diff --git a/server/example.config.toml b/server/example.config.toml index c35dce6b7..41d136b59 100644 --- a/server/example.config.toml +++ b/server/example.config.toml @@ -14,7 +14,7 @@ # Example OpenSandbox configuration. # Copy this file to ~/.sandbox.toml or set SANDBOX_CONFIG_PATH to point at it. -# Each top-level block mirrors the sections supported by src/config.py. +# Each top-level block mirrors the sections supported by opensandbox_server/config.py. [server] # Lifecycle API host/port and logging settings diff --git a/server/example.config.zh.toml b/server/example.config.zh.toml index 77848a878..3940d0ae2 100644 --- a/server/example.config.zh.toml +++ b/server/example.config.zh.toml @@ -14,7 +14,7 @@ # Example OpenSandbox configuration. # Copy this file to ~/.sandbox.toml or set SANDBOX_CONFIG_PATH to point at it. -# Each top-level block mirrors the sections supported by src/config.py. +# Each top-level block mirrors the sections supported by opensandbox_server/config.py. [server] # Lifecycle API host/port and logging settings diff --git a/server/src/__init__.py b/server/opensandbox_server/__init__.py similarity index 100% rename from server/src/__init__.py rename to server/opensandbox_server/__init__.py diff --git a/server/src/api/__init__.py b/server/opensandbox_server/api/__init__.py similarity index 100% rename from server/src/api/__init__.py rename to server/opensandbox_server/api/__init__.py diff --git a/server/src/api/lifecycle.py b/server/opensandbox_server/api/lifecycle.py similarity index 98% rename from server/src/api/lifecycle.py rename to server/opensandbox_server/api/lifecycle.py index e2deb800c..3923e4344 100644 --- a/server/src/api/lifecycle.py +++ b/server/opensandbox_server/api/lifecycle.py @@ -24,8 +24,8 @@ from fastapi import APIRouter, Header, Query, Request, status from fastapi.responses import Response -from src.extensions import validate_extensions -from src.api.schema import ( +from opensandbox_server.extensions import validate_extensions +from opensandbox_server.api.schema import ( CreateSandboxRequest, CreateSandboxResponse, Endpoint, @@ -38,7 +38,7 @@ Sandbox, SandboxFilter, ) -from src.services.factory import create_sandbox_service +from opensandbox_server.services.factory import create_sandbox_service # Initialize router router = APIRouter(tags=["Sandboxes"]) diff --git a/server/src/api/pool.py b/server/opensandbox_server/api/pool.py similarity index 96% rename from server/src/api/pool.py rename to server/opensandbox_server/api/pool.py index f2764bc78..4f33f6bdc 100644 --- a/server/src/api/pool.py +++ b/server/opensandbox_server/api/pool.py @@ -25,15 +25,15 @@ from fastapi.exceptions import HTTPException from fastapi.responses import Response -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreatePoolRequest, ErrorResponse, ListPoolsResponse, PoolResponse, UpdatePoolRequest, ) -from src.config import get_config -from src.services.constants import SandboxErrorCodes +from opensandbox_server.config import get_config +from opensandbox_server.services.constants import SandboxErrorCodes router = APIRouter(tags=["Pools"]) @@ -50,8 +50,8 @@ def _get_pool_service(): This deferred approach means the pool router can be registered unconditionally in main.py; non-k8s deployments simply receive a clear 501 on every call. """ - from src.services.k8s.client import K8sClient - from src.services.k8s.pool_service import PoolService + from opensandbox_server.services.k8s.client import K8sClient + from opensandbox_server.services.k8s.pool_service import PoolService config = get_config() if config.runtime.type != "kubernetes": diff --git a/server/src/api/proxy.py b/server/opensandbox_server/api/proxy.py similarity index 98% rename from server/src/api/proxy.py rename to server/opensandbox_server/api/proxy.py index a18cc70e4..1f0f2387e 100644 --- a/server/src/api/proxy.py +++ b/server/opensandbox_server/api/proxy.py @@ -30,9 +30,9 @@ from websockets.asyncio.client import ClientConnection from websockets.typing import Origin -from src.api import lifecycle -from src.api.schema import Endpoint -from src.middleware.auth import SANDBOX_API_KEY_HEADER +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import Endpoint +from opensandbox_server.middleware.auth import SANDBOX_API_KEY_HEADER logger = logging.getLogger(__name__) diff --git a/server/src/api/schema.py b/server/opensandbox_server/api/schema.py similarity index 100% rename from server/src/api/schema.py rename to server/opensandbox_server/api/schema.py diff --git a/server/src/cli.py b/server/opensandbox_server/cli.py similarity index 95% rename from server/src/cli.py rename to server/opensandbox_server/cli.py index 5d3232675..02a04e953 100644 --- a/server/src/cli.py +++ b/server/opensandbox_server/cli.py @@ -18,13 +18,14 @@ import os import shutil import types +from importlib import resources from pathlib import Path from typing import Any, FrozenSet, Union, get_args, get_origin import uvicorn from pydantic import BaseModel -from src.config import ( +from opensandbox_server.config import ( AgentSandboxRuntimeConfig, CONFIG_ENV_VAR, DEFAULT_CONFIG_PATH, @@ -124,16 +125,17 @@ def copy_example_config( raise ValueError(f"Unsupported example kind '{kind}'. Choices: {supported}") filename = EXAMPLE_FILE_MAP[kind] - src_path = Path(__file__).resolve().parent.parent / filename - if not src_path.exists(): - raise FileNotFoundError(f"Missing example config template at {src_path}") - dest_path = Path(destination or DEFAULT_CONFIG_PATH).expanduser() dest_path.parent.mkdir(parents=True, exist_ok=True) if dest_path.exists() and not force: raise FileExistsError(f"Config file already exists at {dest_path}. Use --force to overwrite.") - shutil.copyfile(src_path, dest_path) + example_resource = resources.files("opensandbox_server.examples").joinpath(filename) + if not example_resource.is_file(): + raise FileNotFoundError(f"Missing packaged example config template: {filename}") + + with resources.as_file(example_resource) as src_path: + shutil.copyfile(src_path, dest_path) return dest_path @@ -282,10 +284,10 @@ def main() -> None: if args.config: os.environ[CONFIG_ENV_VAR] = args.config - from src import main as server_main # local import after env is set + from opensandbox_server import main as server_main # local import after env is set uvicorn.run( - "src.main:app", + "opensandbox_server.main:app", host=server_main.app_config.server.host, port=server_main.app_config.server.port, reload=args.reload, diff --git a/server/src/config.py b/server/opensandbox_server/config.py similarity index 100% rename from server/src/config.py rename to server/opensandbox_server/config.py diff --git a/server/src/py.typed b/server/opensandbox_server/examples/.gitkeep similarity index 100% rename from server/src/py.typed rename to server/opensandbox_server/examples/.gitkeep diff --git a/server/opensandbox_server/examples/__init__.py b/server/opensandbox_server/examples/__init__.py new file mode 100644 index 000000000..2ea9304b9 --- /dev/null +++ b/server/opensandbox_server/examples/__init__.py @@ -0,0 +1 @@ +"""Packaged example configuration templates for the OpenSandbox server.""" diff --git a/server/opensandbox_server/examples/example.batchsandbox-template.yaml b/server/opensandbox_server/examples/example.batchsandbox-template.yaml new file mode 100644 index 000000000..d207d9c03 --- /dev/null +++ b/server/opensandbox_server/examples/example.batchsandbox-template.yaml @@ -0,0 +1,18 @@ +# Example BatchSandbox CR template for OpenSandbox Kubernetes runtime +# This is a complete BatchSandbox CR template that will be merged with runtime values +# +# Usage in config.toml: +# [kubernetes] +# batchsandbox_template_file = "/path/to/this/file.yaml" + +# Metadata template (will be merged with runtime-generated metadata) +metadata: +# Spec template +spec: + replicas: 1 + # Pod template specification + template: + spec: + restartPolicy: Never + tolerations: + - operator: "Exists" diff --git a/server/opensandbox_server/examples/example.config.k8s.toml b/server/opensandbox_server/examples/example.config.k8s.toml new file mode 100644 index 000000000..b37d543fd --- /dev/null +++ b/server/opensandbox_server/examples/example.config.k8s.toml @@ -0,0 +1,83 @@ +# 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. + +# Example Kubernetes Runtime Configuration for OpenSandbox Server +# +# This configuration file demonstrates how to configure the OpenSandbox server +# to use Kubernetes as the sandbox runtime. +# +# Usage: +# 1. Copy this file to ~/.sandbox.toml (or set SANDBOX_CONFIG_PATH environment variable) +# 2. Update the configuration values according to your environment +# 3. Start the server: uvicorn opensandbox_server.main:app --host 0.0.0.0 --port 8080 + +[server] +host = "0.0.0.0" +port = 8080 +log_level = "INFO" +# api_key = "your-secret-api-key" # Optional: Uncomment to enable API key authentication + +# 🧪 [EXPERIMENTAL] Renew-on-access. Off by default — see server/README.md. +[renew_intent] +enabled = false +min_interval_seconds = 60 +redis.enabled = false +# redis.dsn = "redis://127.0.0.1:6379/0" +redis.queue_key = "opensandbox:renew:intent" +redis.consumer_concurrency = 8 + +[runtime] +type = "kubernetes" +execd_image = "opensandbox/execd:v1.0.7" + +[storage] +# Volume and storage configuration +# ----------------------------------------------------------------- +# Allowlist of host path prefixes permitted for bind mounts. +# If empty, all host paths are allowed (not recommended for production). +# Example: allowed_host_paths = ["/data/opensandbox", "/tmp/sandbox"] +allowed_host_paths = [] + +[kubernetes] +# Path to kubeconfig file. Leave as null to use in-cluster configuration +# Replace with your path +kubeconfig_path = "~/.kube/config" + +# Namespace for sandbox workloads +namespace = "opensandbox" + +# [Beta] Enable informer-backed cache to reduce API calls. +# Set to false to disable the watch-based cache. +informer_enabled = true +informer_resync_seconds = 300 +informer_watch_timeout_seconds = 60 + +# Workload provider type: available providers are registered in the provider factory +# If not specified, uses the first registered provider (typically "batchsandbox") +workload_provider = "batchsandbox" + +# Path to the BatchSandbox template file +# Replace with your path +batchsandbox_template_file = "~/batchsandbox-template.yaml" + +[ingress] +# Ingress exposure mode: direct (default) or gateway +mode = "direct" + +[egress] +# Egress configuration +# ----------------------------------------------------------------- +image = "opensandbox/egress:v1.0.3" +# Enforcement: "dns" (DNS proxy only) or "dns+nft" (nftables + DNS). +mode = "dns" diff --git a/server/opensandbox_server/examples/example.config.k8s.zh.toml b/server/opensandbox_server/examples/example.config.k8s.zh.toml new file mode 100644 index 000000000..20915a474 --- /dev/null +++ b/server/opensandbox_server/examples/example.config.k8s.zh.toml @@ -0,0 +1,84 @@ +# 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. + +# Example Kubernetes Runtime Configuration for OpenSandbox Server +# +# This configuration file demonstrates how to configure the OpenSandbox server +# to use Kubernetes as the sandbox runtime. +# +# Usage: +# 1. Copy this file to ~/.sandbox.toml (or set SANDBOX_CONFIG_PATH environment variable) +# 2. Update the configuration values according to your environment +# 3. Start the server: uvicorn opensandbox_server.main:app --host 0.0.0.0 --port 8080 + +[server] +host = "0.0.0.0" +port = 8080 +log_level = "INFO" +# api_key = "your-secret-api-key" # Optional: Uncomment to enable API key authentication + +# 🧪 [EXPERIMENTAL] 按访问续期。默认关闭 — 见 server/README_zh.md。 +[renew_intent] +enabled = false +min_interval_seconds = 60 +redis.enabled = false +# redis.dsn = "redis://127.0.0.1:6379/0" +redis.queue_key = "opensandbox:renew:intent" +redis.consumer_concurrency = 8 + +[runtime] +type = "kubernetes" +execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.7" + +[storage] +# 卷存储配置 +# ----------------------------------------------------------------- +# 允许进行 bind mount 的宿主机路径前缀白名单。 +# 仅匹配这些前缀的路径才能被挂载到沙箱中。 +# 如果为空,则允许所有路径(不建议在生产环境使用)。 +# 示例:allowed_host_paths = ["/data/opensandbox", "/tmp/sandbox"] +allowed_host_paths = [] + +[kubernetes] +# Path to kubeconfig file. Leave as null to use in-cluster configuration +# Replace with your path +kubeconfig_path = "~/.kube/config" + +# Namespace for sandbox workloads +namespace = "opensandbox" + +# [Beta] 启用 informer 缓存以减少 API 调用。 +# 如需关闭 watch 缓存,将该项设为 false。 +informer_enabled = true +informer_resync_seconds = 300 +informer_watch_timeout_seconds = 60 + +# Workload provider type: available providers are registered in the provider factory +# If not specified, uses the first registered provider (typically "batchsandbox") +workload_provider = "batchsandbox" + +# Path to the BatchSandbox template file +# Replace with your path +batchsandbox_template_file = "~/batchsandbox-template.yaml" + +[ingress] +# Ingress exposure mode: direct (default) or gateway +mode = "direct" + +[egress] +# Egress configuration +# ----------------------------------------------------------------- +image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.0.3" +# Enforcement: "dns" (DNS proxy only) or "dns+nft" (nftables + DNS). +mode = "dns" diff --git a/server/opensandbox_server/examples/example.config.toml b/server/opensandbox_server/examples/example.config.toml new file mode 100644 index 000000000..41d136b59 --- /dev/null +++ b/server/opensandbox_server/examples/example.config.toml @@ -0,0 +1,84 @@ +# 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. + +# Example OpenSandbox configuration. +# Copy this file to ~/.sandbox.toml or set SANDBOX_CONFIG_PATH to point at it. +# Each top-level block mirrors the sections supported by opensandbox_server/config.py. + +[server] +# Lifecycle API host/port and logging settings +# ----------------------------------------------------------------- +host = "127.0.0.1" +port = 8080 +log_level = "INFO" +# api_key = "your-secret-api-key" # Optional: Uncomment to enable API key authentication +# eip = "1.2.3.4" # Optional: External IP/hostname for endpoint URLs when returning sandbox endpoints +# Maximum TTL for sandboxes that specify timeout. Comment out this line to disable the upper bound. +max_sandbox_timeout_seconds = 86400 + +# 🧪 [EXPERIMENTAL] Renew-on-access (OSEP-0009). Off by default — see server/README.md. +[renew_intent] +enabled = false +min_interval_seconds = 60 +redis.enabled = false +# redis.dsn = "redis://127.0.0.1:6379/0" +redis.queue_key = "opensandbox:renew:intent" +redis.consumer_concurrency = 8 + +[runtime] +# Runtime selection (docker | kubernetes) +# ----------------------------------------------------------------- +type = "docker" +execd_image = "opensandbox/execd:v1.0.7" + +[egress] +# Egress configuration +# ----------------------------------------------------------------- +image = "opensandbox/egress:v1.0.3" +# Enforcement: "dns" (DNS proxy only) or "dns+nft" (nftables + DNS). +mode = "dns" + +[storage] +# Volume and storage configuration +# ----------------------------------------------------------------- +# Allowlist of host path prefixes permitted for bind mounts. +# If empty, all host paths are allowed (not recommended for production). +# Example: allowed_host_paths = ["/data/opensandbox", "/tmp/sandbox"] +allowed_host_paths = [] + +[docker] +# Docker-specific knobs +# ----------------------------------------------------------------- +# Use bridge for network isolation +network_mode = "bridge" +# Docker API timeout (seconds). If unset, default 180 +# api_timeout = 300 +# When server runs in a container, host IP/hostname for bridge-mode endpoints +# host_ip = "10.57.1.91" +# Drop dangerous capabilities and block privilege escalation +drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"] +no_new_privileges = true +# Optional: set an AppArmor profile name (e.g., "docker-default") when AppArmor is enabled +apparmor_profile = "" +# Limit process count to reduce host impact from fork bombs; set to null to disable +# TODO: For production environments, it is recommended to set this to '4096' or higher to avoid +# "can't start new thread" errors when multiple sandboxes are running concurrently. +# See: https://github.com/alibaba/OpenSandbox/issues/447 +pids_limit = 4096 +# Seccomp profile: empty string uses Docker default; set to an absolute path for a custom profile +seccomp_profile = "" + +[ingress] +# Ingress exposure mode: direct (default) or gateway +mode = "direct" diff --git a/server/opensandbox_server/examples/example.config.zh.toml b/server/opensandbox_server/examples/example.config.zh.toml new file mode 100644 index 000000000..3940d0ae2 --- /dev/null +++ b/server/opensandbox_server/examples/example.config.zh.toml @@ -0,0 +1,77 @@ +# 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. + +# Example OpenSandbox configuration. +# Copy this file to ~/.sandbox.toml or set SANDBOX_CONFIG_PATH to point at it. +# Each top-level block mirrors the sections supported by opensandbox_server/config.py. + +[server] +# Lifecycle API host/port and logging settings +# ----------------------------------------------------------------- +host = "127.0.0.1" +port = 8080 +log_level = "INFO" +# api_key = "your-secret-api-key" # Optional: Uncomment to enable API key authentication + +# 🧪 [EXPERIMENTAL] 按访问续期(OSEP-0009)。默认关闭 — 说明见 server/README_zh.md。 +[renew_intent] +enabled = false +min_interval_seconds = 60 +redis.enabled = false +# redis.dsn = "redis://127.0.0.1:6379/0" +redis.queue_key = "opensandbox:renew:intent" +redis.consumer_concurrency = 8 + +[runtime] +# Runtime selection (docker | kubernetes) +# ----------------------------------------------------------------- +type = "docker" +execd_image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/execd:v1.0.7" + +[egress] +# Egress configuration +# ----------------------------------------------------------------- +image = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/egress:v1.0.3" +# Enforcement: "dns" (DNS proxy only) or "dns+nft" (nftables + DNS). +mode = "dns" + +[storage] +# 卷存储配置 +# ----------------------------------------------------------------- +# 允许进行 bind mount 的宿主机路径前缀白名单。 +# 仅匹配这些前缀的路径才能被挂载到沙箱中。 +# 如果为空,则允许所有路径(不建议在生产环境使用)。 +# 示例:allowed_host_paths = ["/data/opensandbox", "/tmp/sandbox"] +allowed_host_paths = [] + +[docker] +# Docker-specific knobs +# ----------------------------------------------------------------- +# Supported values for network_mode: "host", "bridge" +network_mode = "bridge" +# Drop dangerous capabilities and block privilege escalation +drop_capabilities = ["AUDIT_WRITE", "MKNOD", "NET_ADMIN", "NET_RAW", "SYS_ADMIN", "SYS_MODULE", "SYS_PTRACE", "SYS_TIME", "SYS_TTY_CONFIG"] +no_new_privileges = true +# Optional: set an AppArmor profile name (e.g., "docker-default") when AppArmor is enabled +apparmor_profile = "" +# Limit process count to reduce host impact from fork bombs; set to null to disable +# TODO: 生产环境建议设置为 4096 或更高,避免多沙箱并发时出现 "can't start new thread" 错误 +# See: https://github.com/alibaba/OpenSandbox/issues/447 +pids_limit = 4096 +# Seccomp profile: empty string uses Docker default; set to an absolute path for a custom profile +seccomp_profile = "" + +[ingress] +# Ingress exposure mode: direct (default) or gateway +mode = "direct" diff --git a/server/src/extensions/__init__.py b/server/opensandbox_server/extensions/__init__.py similarity index 85% rename from server/src/extensions/__init__.py rename to server/opensandbox_server/extensions/__init__.py index 36ac51719..69f3814c6 100644 --- a/server/src/extensions/__init__.py +++ b/server/opensandbox_server/extensions/__init__.py @@ -16,12 +16,12 @@ CreateSandbox ``extensions`` shared logic: well-known keys, HTTP validation, workload storage codec. """ -from src.extensions.codec import apply_access_renew_extend_seconds_to_mapping -from src.extensions.keys import ( +from opensandbox_server.extensions.codec import apply_access_renew_extend_seconds_to_mapping +from opensandbox_server.extensions.keys import ( ACCESS_RENEW_EXTEND_SECONDS_KEY, ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY, ) -from src.extensions.validation import ( +from opensandbox_server.extensions.validation import ( ACCESS_RENEW_EXTEND_SECONDS_MAX, ACCESS_RENEW_EXTEND_SECONDS_MIN, validate_extensions, diff --git a/server/src/extensions/codec.py b/server/opensandbox_server/extensions/codec.py similarity index 96% rename from server/src/extensions/codec.py rename to server/opensandbox_server/extensions/codec.py index 3f4ac8534..2bd793fb9 100644 --- a/server/src/extensions/codec.py +++ b/server/opensandbox_server/extensions/codec.py @@ -16,7 +16,7 @@ from typing import Dict, MutableMapping, Optional -from src.extensions.keys import ( +from opensandbox_server.extensions.keys import ( ACCESS_RENEW_EXTEND_SECONDS_KEY, ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY, ) diff --git a/server/src/extensions/keys.py b/server/opensandbox_server/extensions/keys.py similarity index 100% rename from server/src/extensions/keys.py rename to server/opensandbox_server/extensions/keys.py diff --git a/server/src/extensions/validation.py b/server/opensandbox_server/extensions/validation.py similarity index 96% rename from server/src/extensions/validation.py rename to server/opensandbox_server/extensions/validation.py index 2287a76b8..a4b1ce5ae 100644 --- a/server/src/extensions/validation.py +++ b/server/opensandbox_server/extensions/validation.py @@ -18,8 +18,8 @@ from fastapi import HTTPException, status -from src.extensions.keys import ACCESS_RENEW_EXTEND_SECONDS_KEY -from src.services.constants import SandboxErrorCodes +from opensandbox_server.extensions.keys import ACCESS_RENEW_EXTEND_SECONDS_KEY +from opensandbox_server.services.constants import SandboxErrorCodes ACCESS_RENEW_EXTEND_SECONDS_MIN = 300 # 5 minutes ACCESS_RENEW_EXTEND_SECONDS_MAX = 86400 # 24 hours diff --git a/server/src/integrations/__init__.py b/server/opensandbox_server/integrations/__init__.py similarity index 100% rename from server/src/integrations/__init__.py rename to server/opensandbox_server/integrations/__init__.py diff --git a/server/src/integrations/renew_intent/__init__.py b/server/opensandbox_server/integrations/renew_intent/__init__.py similarity index 76% rename from server/src/integrations/renew_intent/__init__.py rename to server/opensandbox_server/integrations/renew_intent/__init__.py index 3ed6ff7e4..b95698814 100644 --- a/server/src/integrations/renew_intent/__init__.py +++ b/server/opensandbox_server/integrations/renew_intent/__init__.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from src.integrations.renew_intent.consumer import RenewIntentConsumer, start_renew_intent_consumer -from src.integrations.renew_intent.runner import RenewIntentRunner, start_renew_intent_runner +from opensandbox_server.integrations.renew_intent.consumer import RenewIntentConsumer, start_renew_intent_consumer +from opensandbox_server.integrations.renew_intent.runner import RenewIntentRunner, start_renew_intent_runner __all__ = [ "RenewIntentConsumer", diff --git a/server/src/integrations/renew_intent/constants.py b/server/opensandbox_server/integrations/renew_intent/constants.py similarity index 100% rename from server/src/integrations/renew_intent/constants.py rename to server/opensandbox_server/integrations/renew_intent/constants.py diff --git a/server/src/integrations/renew_intent/consumer.py b/server/opensandbox_server/integrations/renew_intent/consumer.py similarity index 94% rename from server/src/integrations/renew_intent/consumer.py rename to server/opensandbox_server/integrations/renew_intent/consumer.py index 742381281..35cc74dc7 100644 --- a/server/src/integrations/renew_intent/consumer.py +++ b/server/opensandbox_server/integrations/renew_intent/consumer.py @@ -27,25 +27,25 @@ from redis.exceptions import RedisError -from src.config import AppConfig -from src.integrations.renew_intent.constants import ( +from opensandbox_server.config import AppConfig +from opensandbox_server.integrations.renew_intent.constants import ( BRPOP_TIMEOUT_SECONDS, INTENT_MAX_AGE_SECONDS, PROXY_RENEW_MAX_TRACKED_SANDBOXES, ) -from src.integrations.renew_intent.controller import AccessRenewController -from src.integrations.renew_intent.intent import parse_renew_intent_json -from src.integrations.renew_intent.logutil import ( +from opensandbox_server.integrations.renew_intent.controller import AccessRenewController +from opensandbox_server.integrations.renew_intent.intent import parse_renew_intent_json +from opensandbox_server.integrations.renew_intent.logutil import ( RENEW_EVENT_WORKERS_NOT_STARTED, RENEW_EVENT_WORKERS_STARTED, RENEW_SOURCE_REDIS_QUEUE, RENEW_SOURCE_SERVER_PROXY, renew_bundle, ) -from src.integrations.renew_intent.redis_client import connect_renew_intent_redis_from_config -from src.services.extension_service import ExtensionService, require_extension_service -from src.services.factory import create_sandbox_service -from src.services.sandbox_service import SandboxService +from opensandbox_server.integrations.renew_intent.redis_client import connect_renew_intent_redis_from_config +from opensandbox_server.services.extension_service import ExtensionService, require_extension_service +from opensandbox_server.services.factory import create_sandbox_service +from opensandbox_server.services.sandbox_service import SandboxService if TYPE_CHECKING: from redis.asyncio import Redis diff --git a/server/src/integrations/renew_intent/controller.py b/server/opensandbox_server/integrations/renew_intent/controller.py similarity index 92% rename from server/src/integrations/renew_intent/controller.py rename to server/opensandbox_server/integrations/renew_intent/controller.py index d65bff53b..cc7ca84a3 100644 --- a/server/src/integrations/renew_intent/controller.py +++ b/server/opensandbox_server/integrations/renew_intent/controller.py @@ -23,9 +23,9 @@ from fastapi import HTTPException -from src.api.schema import RenewSandboxExpirationRequest -from src.integrations.renew_intent.intent import RenewIntent -from src.integrations.renew_intent.logutil import ( +from opensandbox_server.api.schema import RenewSandboxExpirationRequest +from opensandbox_server.integrations.renew_intent.intent import RenewIntent +from opensandbox_server.integrations.renew_intent.logutil import ( RENEW_EVENT_FAILED, RENEW_EVENT_SUCCEEDED, RENEW_SOURCE_REDIS_QUEUE, @@ -34,8 +34,8 @@ ) if TYPE_CHECKING: - from src.services.extension_service import ExtensionService - from src.services.sandbox_service import SandboxService + from opensandbox_server.services.extension_service import ExtensionService + from opensandbox_server.services.sandbox_service import SandboxService logger = logging.getLogger(__name__) diff --git a/server/src/integrations/renew_intent/intent.py b/server/opensandbox_server/integrations/renew_intent/intent.py similarity index 100% rename from server/src/integrations/renew_intent/intent.py rename to server/opensandbox_server/integrations/renew_intent/intent.py diff --git a/server/src/integrations/renew_intent/logutil.py b/server/opensandbox_server/integrations/renew_intent/logutil.py similarity index 100% rename from server/src/integrations/renew_intent/logutil.py rename to server/opensandbox_server/integrations/renew_intent/logutil.py diff --git a/server/src/integrations/renew_intent/proxy_renew.py b/server/opensandbox_server/integrations/renew_intent/proxy_renew.py similarity index 90% rename from server/src/integrations/renew_intent/proxy_renew.py rename to server/opensandbox_server/integrations/renew_intent/proxy_renew.py index 9204c679c..2b72e2af0 100644 --- a/server/src/integrations/renew_intent/proxy_renew.py +++ b/server/opensandbox_server/integrations/renew_intent/proxy_renew.py @@ -19,8 +19,8 @@ from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - from src.config import AppConfig - from src.integrations.renew_intent.consumer import RenewIntentConsumer + from opensandbox_server.config import AppConfig + from opensandbox_server.integrations.renew_intent.consumer import RenewIntentConsumer class ProxyRenewCoordinator: diff --git a/server/src/integrations/renew_intent/redis_client.py b/server/opensandbox_server/integrations/renew_intent/redis_client.py similarity index 94% rename from server/src/integrations/renew_intent/redis_client.py rename to server/opensandbox_server/integrations/renew_intent/redis_client.py index e8e589a36..54139444b 100644 --- a/server/src/integrations/renew_intent/redis_client.py +++ b/server/opensandbox_server/integrations/renew_intent/redis_client.py @@ -22,8 +22,8 @@ import redis.asyncio as redis_async from redis.asyncio import Redis -from src.config import AppConfig -from src.integrations.renew_intent.logutil import ( +from opensandbox_server.config import AppConfig +from opensandbox_server.integrations.renew_intent.logutil import ( RENEW_EVENT_REDIS_CONNECTED, RENEW_SOURCE_REDIS_QUEUE, renew_bundle, diff --git a/server/src/integrations/renew_intent/runner.py b/server/opensandbox_server/integrations/renew_intent/runner.py similarity index 81% rename from server/src/integrations/renew_intent/runner.py rename to server/opensandbox_server/integrations/renew_intent/runner.py index b72b4de48..6d955516f 100644 --- a/server/src/integrations/renew_intent/runner.py +++ b/server/opensandbox_server/integrations/renew_intent/runner.py @@ -18,13 +18,13 @@ from typing import Optional -from src.config import AppConfig -from src.integrations.renew_intent.consumer import ( +from opensandbox_server.config import AppConfig +from opensandbox_server.integrations.renew_intent.consumer import ( RenewIntentConsumer, start_renew_intent_consumer, ) -from src.services.extension_service import ExtensionService -from src.services.sandbox_service import SandboxService +from opensandbox_server.services.extension_service import ExtensionService +from opensandbox_server.services.sandbox_service import SandboxService RenewIntentRunner = RenewIntentConsumer diff --git a/server/src/main.py b/server/opensandbox_server/main.py similarity index 85% rename from server/src/main.py rename to server/opensandbox_server/main.py index a873d5fe5..59bf5c534 100644 --- a/server/src/main.py +++ b/server/opensandbox_server/main.py @@ -30,8 +30,8 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -from src.config import load_config -from src.integrations.renew_intent import start_renew_intent_consumer +from opensandbox_server.config import load_config +from opensandbox_server.integrations.renew_intent import start_renew_intent_consumer from uvicorn.config import LOGGING_CONFIG as UVICORN_LOGGING_CONFIG # Load configuration before initializing routers/middleware @@ -44,7 +44,7 @@ # Inject request_id into log records so one request's logs can be correlated. _log_config["filters"] = { - "request_id": {"()": "src.middleware.request_id.RequestIdFilter"}, + "request_id": {"()": "opensandbox_server.middleware.request_id.RequestIdFilter"}, } _log_config["handlers"]["default"]["filters"] = ["request_id"] _log_config["handlers"]["access"]["filters"] = ["request_id"] @@ -58,8 +58,8 @@ _log_config["formatters"]["access"]["datefmt"] = _datefmt _log_config["formatters"]["access"]["use_colors"] = True -# Ensure project loggers (src.*) emit at configured level using the default handler. -_log_config["loggers"]["src"] = { +# Ensure project loggers emit at configured level using the default handler. +_log_config["loggers"]["opensandbox_server"] = { "handlers": ["default"], "level": app_config.server.log_level.upper(), "propagate": False, @@ -70,14 +70,14 @@ getattr(logging, app_config.server.log_level.upper(), logging.INFO) ) -from src.api.pool import router as pool_router # noqa: E402 -from src.api.lifecycle import router, sandbox_service # noqa: E402 -from src.api.proxy import router as proxy_router # noqa: E402 -from src.integrations.renew_intent.proxy_renew import ProxyRenewCoordinator # noqa: E402 -from src.middleware.auth import AuthMiddleware # noqa: E402 -from src.middleware.request_id import RequestIdMiddleware # noqa: E402 -from src.services.extension_service import require_extension_service # noqa: E402 -from src.services.runtime_resolver import ( # noqa: E402 +from opensandbox_server.api.pool import router as pool_router # noqa: E402 +from opensandbox_server.api.lifecycle import router, sandbox_service # noqa: E402 +from opensandbox_server.api.proxy import router as proxy_router # noqa: E402 +from opensandbox_server.integrations.renew_intent.proxy_renew import ProxyRenewCoordinator # noqa: E402 +from opensandbox_server.middleware.auth import AuthMiddleware # noqa: E402 +from opensandbox_server.middleware.request_id import RequestIdMiddleware # noqa: E402 +from opensandbox_server.services.extension_service import require_extension_service # noqa: E402 +from opensandbox_server.services.runtime_resolver import ( # noqa: E402 validate_secure_runtime_on_startup, ) @@ -100,7 +100,7 @@ async def lifespan(app: FastAPI): docker_client = docker.from_env() logger.info("Validating secure runtime for Docker backend") elif runtime_type == "kubernetes": - from src.services.k8s.client import K8sClient + from opensandbox_server.services.k8s.client import K8sClient k8s_client = K8sClient(app_config.kubernetes) logger.info("Validating secure runtime for Kubernetes backend") @@ -217,7 +217,7 @@ async def health_check(): # Run the application uvicorn.run( - "src.main:app", + "opensandbox_server.main:app", host=app_config.server.host, port=app_config.server.port, reload=True, diff --git a/server/src/middleware/__init__.py b/server/opensandbox_server/middleware/__init__.py similarity index 100% rename from server/src/middleware/__init__.py rename to server/opensandbox_server/middleware/__init__.py diff --git a/server/src/middleware/auth.py b/server/opensandbox_server/middleware/auth.py similarity index 98% rename from server/src/middleware/auth.py rename to server/opensandbox_server/middleware/auth.py index 7a73130d9..323bdc9d4 100644 --- a/server/src/middleware/auth.py +++ b/server/opensandbox_server/middleware/auth.py @@ -26,7 +26,7 @@ from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware -from src.config import AppConfig, get_config +from opensandbox_server.config import AppConfig, get_config SANDBOX_API_KEY_HEADER = "OPEN-SANDBOX-API-KEY" diff --git a/server/src/middleware/request_id.py b/server/opensandbox_server/middleware/request_id.py similarity index 100% rename from server/src/middleware/request_id.py rename to server/opensandbox_server/middleware/request_id.py diff --git a/server/opensandbox_server/py.typed b/server/opensandbox_server/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/services/__init__.py b/server/opensandbox_server/services/__init__.py similarity index 67% rename from server/src/services/__init__.py rename to server/opensandbox_server/services/__init__.py index 3fa510a3c..3a8d2f388 100644 --- a/server/src/services/__init__.py +++ b/server/opensandbox_server/services/__init__.py @@ -14,11 +14,11 @@ """Sandbox service implementations.""" -from src.services.docker import DockerSandboxService -from src.services.extension_service import ExtensionService, require_extension_service -from src.services.k8s.kubernetes_service import KubernetesSandboxService -from src.services.factory import create_sandbox_service -from src.services.sandbox_service import SandboxService +from opensandbox_server.services.docker import DockerSandboxService +from opensandbox_server.services.extension_service import ExtensionService, require_extension_service +from opensandbox_server.services.k8s.kubernetes_service import KubernetesSandboxService +from opensandbox_server.services.factory import create_sandbox_service +from opensandbox_server.services.sandbox_service import SandboxService __all__ = [ "SandboxService", diff --git a/server/src/services/constants.py b/server/opensandbox_server/services/constants.py similarity index 100% rename from server/src/services/constants.py rename to server/opensandbox_server/services/constants.py diff --git a/server/src/services/docker.py b/server/opensandbox_server/services/docker.py similarity index 99% rename from server/src/services/docker.py rename to server/opensandbox_server/services/docker.py index 1b1d9e01e..5b7dbb80b 100644 --- a/server/src/services/docker.py +++ b/server/opensandbox_server/services/docker.py @@ -43,11 +43,11 @@ from docker.errors import DockerException, ImageNotFound, NotFound as DockerNotFound from fastapi import HTTPException, status -from src.extensions import ( +from opensandbox_server.extensions import ( ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY, apply_access_renew_extend_seconds_to_mapping, ) -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreateSandboxRequest, CreateSandboxResponse, Endpoint, @@ -61,9 +61,9 @@ Sandbox, SandboxStatus, ) -from src.config import AppConfig, get_config -from src.services.extension_service import ExtensionService -from src.services.constants import ( +from opensandbox_server.config import AppConfig, get_config +from opensandbox_server.services.extension_service import ExtensionService +from opensandbox_server.services.constants import ( EGRESS_MODE_ENV, EGRESS_RULES_ENV, OPENSANDBOX_EGRESS_TOKEN, @@ -76,21 +76,21 @@ SANDBOX_OSSFS_MOUNTS_LABEL, SandboxErrorCodes, ) -from src.services.endpoint_auth import ( +from opensandbox_server.services.endpoint_auth import ( build_egress_auth_headers, generate_egress_token, merge_endpoint_headers, ) -from src.services.helpers import ( +from opensandbox_server.services.helpers import ( matches_filter, parse_memory_limit, parse_nano_cpus, parse_timestamp, ) -from src.services.ossfs_mixin import OSSFSMixin -from src.services.sandbox_service import SandboxService -from src.services.runtime_resolver import SecureRuntimeResolver -from src.services.validators import ( +from opensandbox_server.services.ossfs_mixin import OSSFSMixin +from opensandbox_server.services.sandbox_service import SandboxService +from opensandbox_server.services.runtime_resolver import SecureRuntimeResolver +from opensandbox_server.services.validators import ( calculate_expiration_or_raise, ensure_egress_configured, ensure_entrypoint, diff --git a/server/src/services/endpoint_auth.py b/server/opensandbox_server/services/endpoint_auth.py similarity index 94% rename from server/src/services/endpoint_auth.py rename to server/opensandbox_server/services/endpoint_auth.py index 839e0ba65..d47c80b2f 100644 --- a/server/src/services/endpoint_auth.py +++ b/server/opensandbox_server/services/endpoint_auth.py @@ -18,7 +18,7 @@ import secrets -from src.services.constants import OPEN_SANDBOX_EGRESS_AUTH_HEADER +from opensandbox_server.services.constants import OPEN_SANDBOX_EGRESS_AUTH_HEADER EGRESS_AUTH_TOKEN_BYTES = 24 diff --git a/server/src/services/extension_service.py b/server/opensandbox_server/services/extension_service.py similarity index 95% rename from server/src/services/extension_service.py rename to server/opensandbox_server/services/extension_service.py index 1e7d493ec..b20c8405e 100644 --- a/server/src/services/extension_service.py +++ b/server/opensandbox_server/services/extension_service.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - from src.services.sandbox_service import SandboxService + from opensandbox_server.services.sandbox_service import SandboxService class ExtensionService(ABC): diff --git a/server/src/services/factory.py b/server/opensandbox_server/services/factory.py similarity index 89% rename from server/src/services/factory.py rename to server/opensandbox_server/services/factory.py index aa6991158..10b9d3d92 100644 --- a/server/src/services/factory.py +++ b/server/opensandbox_server/services/factory.py @@ -22,10 +22,10 @@ import logging from typing import Optional -from src.config import AppConfig, get_config -from src.services.docker import DockerSandboxService -from src.services.k8s import KubernetesSandboxService -from src.services.sandbox_service import SandboxService +from opensandbox_server.config import AppConfig, get_config +from opensandbox_server.services.docker import DockerSandboxService +from opensandbox_server.services.k8s import KubernetesSandboxService +from opensandbox_server.services.sandbox_service import SandboxService logger = logging.getLogger(__name__) diff --git a/server/src/services/helpers.py b/server/opensandbox_server/services/helpers.py similarity index 97% rename from server/src/services/helpers.py rename to server/opensandbox_server/services/helpers.py index 158789730..5c78959fe 100644 --- a/server/src/services/helpers.py +++ b/server/opensandbox_server/services/helpers.py @@ -26,9 +26,9 @@ from datetime import datetime, timezone from typing import Dict, Optional -from src.api.schema import Endpoint, Sandbox, SandboxFilter -from src.services.constants import OPEN_SANDBOX_INGRESS_HEADER -from src.config import ( +from opensandbox_server.api.schema import Endpoint, Sandbox, SandboxFilter +from opensandbox_server.services.constants import OPEN_SANDBOX_INGRESS_HEADER +from opensandbox_server.config import ( GATEWAY_ROUTE_MODE_HEADER, GATEWAY_ROUTE_MODE_URI, GATEWAY_ROUTE_MODE_WILDCARD, diff --git a/server/src/services/k8s/__init__.py b/server/opensandbox_server/services/k8s/__init__.py similarity index 86% rename from server/src/services/k8s/__init__.py rename to server/opensandbox_server/services/k8s/__init__.py index f516a524e..3837da79f 100644 --- a/server/src/services/k8s/__init__.py +++ b/server/opensandbox_server/services/k8s/__init__.py @@ -16,8 +16,8 @@ Kubernetes runtime implementation for OpenSandbox. """ -from src.services.k8s.kubernetes_service import KubernetesSandboxService -from src.services.k8s.provider_factory import ( +from opensandbox_server.services.k8s.kubernetes_service import KubernetesSandboxService +from opensandbox_server.services.k8s.provider_factory import ( create_workload_provider, register_provider, list_available_providers, diff --git a/server/src/services/k8s/agent_sandbox_provider.py b/server/opensandbox_server/services/k8s/agent_sandbox_provider.py similarity index 96% rename from server/src/services/k8s/agent_sandbox_provider.py rename to server/opensandbox_server/services/k8s/agent_sandbox_provider.py index 4834536b9..086927290 100644 --- a/server/src/services/k8s/agent_sandbox_provider.py +++ b/server/opensandbox_server/services/k8s/agent_sandbox_provider.py @@ -29,23 +29,23 @@ V1VolumeMount, ) -from src.config import AppConfig, EGRESS_MODE_DNS -from src.services.helpers import format_ingress_endpoint -from src.api.schema import Endpoint, ImageSpec, NetworkPolicy, Volume -from src.services.k8s.agent_sandbox_template import AgentSandboxTemplateManager -from src.services.k8s.client import K8sClient -from src.services.k8s.egress_helper import ( +from opensandbox_server.config import AppConfig, EGRESS_MODE_DNS +from opensandbox_server.services.helpers import format_ingress_endpoint +from opensandbox_server.api.schema import Endpoint, ImageSpec, NetworkPolicy, Volume +from opensandbox_server.services.k8s.agent_sandbox_template import AgentSandboxTemplateManager +from opensandbox_server.services.k8s.client import K8sClient +from opensandbox_server.services.k8s.egress_helper import ( apply_egress_to_spec, build_security_context_for_sandbox_container, prep_execd_init_for_egress, ) -from src.services.k8s.security_context import ( +from opensandbox_server.services.k8s.security_context import ( build_security_context_from_dict, serialize_security_context_to_dict, ) -from src.services.k8s.volume_helper import apply_volumes_to_pod_spec -from src.services.k8s.workload_provider import WorkloadProvider -from src.services.runtime_resolver import SecureRuntimeResolver +from opensandbox_server.services.k8s.volume_helper import apply_volumes_to_pod_spec +from opensandbox_server.services.k8s.workload_provider import WorkloadProvider +from opensandbox_server.services.runtime_resolver import SecureRuntimeResolver logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/agent_sandbox_template.py b/server/opensandbox_server/services/k8s/agent_sandbox_template.py similarity index 91% rename from server/src/services/k8s/agent_sandbox_template.py rename to server/opensandbox_server/services/k8s/agent_sandbox_template.py index 4dcd40de2..3807b6997 100644 --- a/server/src/services/k8s/agent_sandbox_template.py +++ b/server/opensandbox_server/services/k8s/agent_sandbox_template.py @@ -18,7 +18,7 @@ from typing import Optional -from src.services.k8s.template_manager import BaseSandboxTemplateManager +from opensandbox_server.services.k8s.template_manager import BaseSandboxTemplateManager class AgentSandboxTemplateManager(BaseSandboxTemplateManager): diff --git a/server/src/services/k8s/batchsandbox_provider.py b/server/opensandbox_server/services/k8s/batchsandbox_provider.py similarity index 97% rename from server/src/services/k8s/batchsandbox_provider.py rename to server/opensandbox_server/services/k8s/batchsandbox_provider.py index eab1837f5..c7a49ccd0 100644 --- a/server/src/services/k8s/batchsandbox_provider.py +++ b/server/opensandbox_server/services/k8s/batchsandbox_provider.py @@ -29,27 +29,27 @@ V1VolumeMount, ) -from src.config import AppConfig, EGRESS_MODE_DNS, INGRESS_MODE_GATEWAY -from src.services.helpers import format_ingress_endpoint -from src.api.schema import Endpoint, ImageSpec, NetworkPolicy, Volume -from src.services.k8s.image_pull_secret_helper import ( +from opensandbox_server.config import AppConfig, EGRESS_MODE_DNS, INGRESS_MODE_GATEWAY +from opensandbox_server.services.helpers import format_ingress_endpoint +from opensandbox_server.api.schema import Endpoint, ImageSpec, NetworkPolicy, Volume +from opensandbox_server.services.k8s.image_pull_secret_helper import ( build_image_pull_secret, build_image_pull_secret_name, ) -from src.services.k8s.batchsandbox_template import BatchSandboxTemplateManager -from src.services.k8s.client import K8sClient -from src.services.k8s.egress_helper import ( +from opensandbox_server.services.k8s.batchsandbox_template import BatchSandboxTemplateManager +from opensandbox_server.services.k8s.client import K8sClient +from opensandbox_server.services.k8s.egress_helper import ( apply_egress_to_spec, build_security_context_for_sandbox_container, prep_execd_init_for_egress, ) -from src.services.k8s.security_context import ( +from opensandbox_server.services.k8s.security_context import ( build_security_context_from_dict, serialize_security_context_to_dict, ) -from src.services.k8s.volume_helper import apply_volumes_to_pod_spec -from src.services.k8s.workload_provider import WorkloadProvider -from src.services.runtime_resolver import SecureRuntimeResolver +from opensandbox_server.services.k8s.volume_helper import apply_volumes_to_pod_spec +from opensandbox_server.services.k8s.workload_provider import WorkloadProvider +from opensandbox_server.services.runtime_resolver import SecureRuntimeResolver logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/batchsandbox_template.py b/server/opensandbox_server/services/k8s/batchsandbox_template.py similarity index 91% rename from server/src/services/k8s/batchsandbox_template.py rename to server/opensandbox_server/services/k8s/batchsandbox_template.py index b37c30a07..b7244e82e 100644 --- a/server/src/services/k8s/batchsandbox_template.py +++ b/server/opensandbox_server/services/k8s/batchsandbox_template.py @@ -18,7 +18,7 @@ from typing import Optional -from src.services.k8s.template_manager import BaseSandboxTemplateManager +from opensandbox_server.services.k8s.template_manager import BaseSandboxTemplateManager class BatchSandboxTemplateManager(BaseSandboxTemplateManager): diff --git a/server/src/services/k8s/client.py b/server/opensandbox_server/services/k8s/client.py similarity index 97% rename from server/src/services/k8s/client.py rename to server/opensandbox_server/services/k8s/client.py index ca341a953..a945d6f1d 100644 --- a/server/src/services/k8s/client.py +++ b/server/opensandbox_server/services/k8s/client.py @@ -25,9 +25,9 @@ from kubernetes import client, config from kubernetes.client import ApiException, CoreV1Api, CustomObjectsApi, NodeV1Api -from src.config import KubernetesRuntimeConfig -from src.services.k8s.informer import WorkloadInformer -from src.services.k8s.rate_limiter import TokenBucketRateLimiter +from opensandbox_server.config import KubernetesRuntimeConfig +from opensandbox_server.services.k8s.informer import WorkloadInformer +from opensandbox_server.services.k8s.rate_limiter import TokenBucketRateLimiter logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/egress_helper.py b/server/opensandbox_server/services/k8s/egress_helper.py similarity index 95% rename from server/src/services/k8s/egress_helper.py rename to server/opensandbox_server/services/k8s/egress_helper.py index 8f958514b..d0c82743c 100644 --- a/server/src/services/k8s/egress_helper.py +++ b/server/opensandbox_server/services/k8s/egress_helper.py @@ -22,9 +22,9 @@ import json from typing import Any, Dict, List, Optional -from src.api.schema import NetworkPolicy -from src.config import EGRESS_MODE_DNS -from src.services.constants import ( +from opensandbox_server.api.schema import NetworkPolicy +from opensandbox_server.config import EGRESS_MODE_DNS +from opensandbox_server.services.constants import ( EGRESS_MODE_ENV, EGRESS_RULES_ENV, OPENSANDBOX_EGRESS_TOKEN, diff --git a/server/src/services/k8s/image_pull_secret_helper.py b/server/opensandbox_server/services/k8s/image_pull_secret_helper.py similarity index 98% rename from server/src/services/k8s/image_pull_secret_helper.py rename to server/opensandbox_server/services/k8s/image_pull_secret_helper.py index 248c1c6fc..54c1d0dc7 100644 --- a/server/src/services/k8s/image_pull_secret_helper.py +++ b/server/opensandbox_server/services/k8s/image_pull_secret_helper.py @@ -21,7 +21,7 @@ from kubernetes.client import V1ObjectMeta, V1OwnerReference, V1Secret -from src.api.schema import ImageAuth +from opensandbox_server.api.schema import ImageAuth IMAGE_AUTH_SECRET_PREFIX = "opensandbox-image-auth" diff --git a/server/src/services/k8s/informer.py b/server/opensandbox_server/services/k8s/informer.py similarity index 100% rename from server/src/services/k8s/informer.py rename to server/opensandbox_server/services/k8s/informer.py diff --git a/server/src/services/k8s/kubernetes_service.py b/server/opensandbox_server/services/k8s/kubernetes_service.py similarity index 97% rename from server/src/services/k8s/kubernetes_service.py rename to server/opensandbox_server/services/k8s/kubernetes_service.py index 1ee417c41..090b52500 100644 --- a/server/src/services/k8s/kubernetes_service.py +++ b/server/opensandbox_server/services/k8s/kubernetes_service.py @@ -27,9 +27,9 @@ from fastapi import HTTPException, status -from src.extensions import apply_access_renew_extend_seconds_to_mapping -from src.extensions.keys import ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY -from src.api.schema import ( +from opensandbox_server.extensions import apply_access_renew_extend_seconds_to_mapping +from opensandbox_server.extensions.keys import ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY +from opensandbox_server.api.schema import ( CreateSandboxRequest, CreateSandboxResponse, Endpoint, @@ -42,19 +42,19 @@ Sandbox, SandboxStatus, ) -from src.config import AppConfig, EGRESS_MODE_DNS, get_config -from src.services.constants import ( +from opensandbox_server.config import AppConfig, EGRESS_MODE_DNS, get_config +from opensandbox_server.services.constants import ( SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY, SANDBOX_ID_LABEL, SANDBOX_MANUAL_CLEANUP_LABEL, SandboxErrorCodes, ) -from src.services.endpoint_auth import generate_egress_token -from src.services.endpoint_auth import build_egress_auth_headers, merge_endpoint_headers -from src.services.helpers import matches_filter -from src.services.extension_service import ExtensionService -from src.services.sandbox_service import SandboxService -from src.services.validators import ( +from opensandbox_server.services.endpoint_auth import generate_egress_token +from opensandbox_server.services.endpoint_auth import build_egress_auth_headers, merge_endpoint_headers +from opensandbox_server.services.helpers import matches_filter +from opensandbox_server.services.extension_service import ExtensionService +from opensandbox_server.services.sandbox_service import SandboxService +from opensandbox_server.services.validators import ( calculate_expiration_or_raise, ensure_entrypoint, ensure_egress_configured, @@ -63,8 +63,8 @@ ensure_timeout_within_limit, ensure_volumes_valid, ) -from src.services.k8s.client import K8sClient -from src.services.k8s.provider_factory import create_workload_provider +from opensandbox_server.services.k8s.client import K8sClient +from opensandbox_server.services.k8s.provider_factory import create_workload_provider logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/pool_service.py b/server/opensandbox_server/services/k8s/pool_service.py similarity index 98% rename from server/src/services/k8s/pool_service.py rename to server/opensandbox_server/services/k8s/pool_service.py index e6cffb704..cabb633fc 100644 --- a/server/src/services/k8s/pool_service.py +++ b/server/opensandbox_server/services/k8s/pool_service.py @@ -25,7 +25,7 @@ from fastapi import HTTPException, status from kubernetes.client import ApiException -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreatePoolRequest, ListPoolsResponse, PoolCapacitySpec, @@ -33,8 +33,8 @@ PoolStatus, UpdatePoolRequest, ) -from src.services.constants import SandboxErrorCodes -from src.services.k8s.client import K8sClient +from opensandbox_server.services.constants import SandboxErrorCodes +from opensandbox_server.services.k8s.client import K8sClient logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/provider_factory.py b/server/opensandbox_server/services/k8s/provider_factory.py similarity index 92% rename from server/src/services/k8s/provider_factory.py rename to server/opensandbox_server/services/k8s/provider_factory.py index fc1ba2bc2..2ca38d3af 100644 --- a/server/src/services/k8s/provider_factory.py +++ b/server/opensandbox_server/services/k8s/provider_factory.py @@ -19,11 +19,11 @@ import logging from typing import Dict, Type, Optional -from src.config import AppConfig -from src.services.k8s.workload_provider import WorkloadProvider -from src.services.k8s.batchsandbox_provider import BatchSandboxProvider -from src.services.k8s.agent_sandbox_provider import AgentSandboxProvider -from src.services.k8s.client import K8sClient +from opensandbox_server.config import AppConfig +from opensandbox_server.services.k8s.workload_provider import WorkloadProvider +from opensandbox_server.services.k8s.batchsandbox_provider import BatchSandboxProvider +from opensandbox_server.services.k8s.agent_sandbox_provider import AgentSandboxProvider +from opensandbox_server.services.k8s.client import K8sClient logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/rate_limiter.py b/server/opensandbox_server/services/k8s/rate_limiter.py similarity index 100% rename from server/src/services/k8s/rate_limiter.py rename to server/opensandbox_server/services/k8s/rate_limiter.py diff --git a/server/src/services/k8s/security_context.py b/server/opensandbox_server/services/k8s/security_context.py similarity index 100% rename from server/src/services/k8s/security_context.py rename to server/opensandbox_server/services/k8s/security_context.py diff --git a/server/src/services/k8s/template_manager.py b/server/opensandbox_server/services/k8s/template_manager.py similarity index 100% rename from server/src/services/k8s/template_manager.py rename to server/opensandbox_server/services/k8s/template_manager.py diff --git a/server/src/services/k8s/volume_helper.py b/server/opensandbox_server/services/k8s/volume_helper.py similarity index 98% rename from server/src/services/k8s/volume_helper.py rename to server/opensandbox_server/services/k8s/volume_helper.py index a61d764b0..6f19e09fb 100644 --- a/server/src/services/k8s/volume_helper.py +++ b/server/opensandbox_server/services/k8s/volume_helper.py @@ -19,7 +19,7 @@ import logging from typing import Any, Dict, List -from src.api.schema import Volume +from opensandbox_server.api.schema import Volume logger = logging.getLogger(__name__) diff --git a/server/src/services/k8s/workload_provider.py b/server/opensandbox_server/services/k8s/workload_provider.py similarity index 97% rename from server/src/services/k8s/workload_provider.py rename to server/opensandbox_server/services/k8s/workload_provider.py index 3884db4f0..e4476b50e 100644 --- a/server/src/services/k8s/workload_provider.py +++ b/server/opensandbox_server/services/k8s/workload_provider.py @@ -20,8 +20,8 @@ from datetime import datetime from typing import Dict, List, Any, Optional -from src.api.schema import Endpoint, ImageSpec, NetworkPolicy, Volume -from src.config import EGRESS_MODE_DNS +from opensandbox_server.api.schema import Endpoint, ImageSpec, NetworkPolicy, Volume +from opensandbox_server.config import EGRESS_MODE_DNS class WorkloadProvider(ABC): diff --git a/server/src/services/ossfs_mixin.py b/server/opensandbox_server/services/ossfs_mixin.py similarity index 99% rename from server/src/services/ossfs_mixin.py rename to server/opensandbox_server/services/ossfs_mixin.py index dec1b0364..40e8a0364 100644 --- a/server/src/services/ossfs_mixin.py +++ b/server/opensandbox_server/services/ossfs_mixin.py @@ -27,8 +27,8 @@ from fastapi import HTTPException, status -from src.services.constants import SandboxErrorCodes -from src.services.helpers import normalize_external_endpoint_url +from opensandbox_server.services.constants import SandboxErrorCodes +from opensandbox_server.services.helpers import normalize_external_endpoint_url logger = logging.getLogger(__name__) diff --git a/server/src/services/runtime_resolver.py b/server/opensandbox_server/services/runtime_resolver.py similarity index 98% rename from server/src/services/runtime_resolver.py rename to server/opensandbox_server/services/runtime_resolver.py index 1afc98d77..240ef6b3a 100644 --- a/server/src/services/runtime_resolver.py +++ b/server/opensandbox_server/services/runtime_resolver.py @@ -33,8 +33,8 @@ if TYPE_CHECKING: from docker import DockerClient - from src.config import AppConfig, SecureRuntimeConfig - from src.services.k8s.client import K8sClient + from opensandbox_server.config import AppConfig, SecureRuntimeConfig + from opensandbox_server.services.k8s.client import K8sClient class SecureRuntimeResolver: diff --git a/server/src/services/sandbox_service.py b/server/opensandbox_server/services/sandbox_service.py similarity index 98% rename from server/src/services/sandbox_service.py rename to server/opensandbox_server/services/sandbox_service.py index 794331a26..f4cd9d319 100644 --- a/server/src/services/sandbox_service.py +++ b/server/opensandbox_server/services/sandbox_service.py @@ -23,7 +23,7 @@ import socket from uuid import uuid4 -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreateSandboxRequest, CreateSandboxResponse, Endpoint, @@ -33,7 +33,7 @@ RenewSandboxExpirationResponse, Sandbox, ) -from src.services.validators import ensure_valid_port +from opensandbox_server.services.validators import ensure_valid_port class SandboxService(ABC): diff --git a/server/src/services/validators.py b/server/opensandbox_server/services/validators.py similarity index 98% rename from server/src/services/validators.py rename to server/opensandbox_server/services/validators.py index 178fd7627..daed0acfb 100644 --- a/server/src/services/validators.py +++ b/server/opensandbox_server/services/validators.py @@ -28,11 +28,11 @@ from fastapi import HTTPException, status import re -from src.services.constants import RESERVED_LABEL_PREFIX, SandboxErrorCodes +from opensandbox_server.services.constants import RESERVED_LABEL_PREFIX, SandboxErrorCodes if TYPE_CHECKING: - from src.api.schema import NetworkPolicy, OSSFS, Volume - from src.config import EgressConfig + from opensandbox_server.api.schema import NetworkPolicy, OSSFS, Volume + from opensandbox_server.config import EgressConfig def ensure_entrypoint(entrypoint: Sequence[str]) -> None: diff --git a/server/pyproject.toml b/server/pyproject.toml index b49661d92..5bf44ab97 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -61,7 +61,7 @@ Repository = "https://github.com/alibaba/OpenSandbox" Issues = "https://github.com/alibaba/OpenSandbox/issues" [project.scripts] -opensandbox-server = "src.cli:main" +opensandbox-server = "opensandbox_server.cli:main" [tool.hatch.version] source = "vcs" @@ -76,24 +76,11 @@ fallback_version = "0.1.0.dev0" [tool.hatch.build] include = [ "LICENSE", - "example.config.toml", - "example.config.zh.toml", - "example.config.k8s.toml", - "example.config.k8s.zh.toml", - "example.batchsandbox-template.yaml", - "src/**/py.typed", - "src", + "opensandbox_server", ] [tool.hatch.build.targets.wheel] -packages = ["src"] - -[tool.hatch.build.targets.wheel.force-include] -"example.config.toml" = "example.config.toml" -"example.config.zh.toml" = "example.config.zh.toml" -"example.config.k8s.toml" = "example.config.k8s.toml" -"example.config.k8s.zh.toml" = "example.config.k8s.zh.toml" -"example.batchsandbox-template.yaml" = "example.batchsandbox-template.yaml" +packages = ["opensandbox_server"] [dependency-groups] dev = [ @@ -107,7 +94,7 @@ dev = [ [tool.ruff] target-version = "py310" line-length = 100 -src = ["src", "tests"] +src = ["opensandbox_server", "tests"] [tool.ruff.lint] select = ["E4", "E7", "E9", "F"] @@ -117,7 +104,7 @@ typeCheckingMode = "standard" pythonVersion = "3.10" pythonPlatform = "All" -include = ["src", "tests"] +include = ["opensandbox_server", "tests"] exclude = [ "**/node_modules", diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 8acbfba80..6fc8dff38 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -35,7 +35,7 @@ _mock_docker_client.containers.list.return_value = [] docker.from_env = lambda: _mock_docker_client # type: ignore -from src.main import app # noqa: E402 +from opensandbox_server.main import app # noqa: E402 @pytest.fixture(scope="session") diff --git a/server/tests/k8s/conftest.py b/server/tests/k8s/conftest.py index 30b72c9e8..0a080a9ac 100644 --- a/server/tests/k8s/conftest.py +++ b/server/tests/k8s/conftest.py @@ -48,5 +48,5 @@ def update_cache(self, obj): return None monkeypatch.setattr( - "src.services.k8s.client.WorkloadInformer", _FakeInformer + "opensandbox_server.services.k8s.client.WorkloadInformer", _FakeInformer ) diff --git a/server/tests/k8s/fixtures/k8s_fixtures.py b/server/tests/k8s/fixtures/k8s_fixtures.py index 81dae4ed4..2a07c035f 100644 --- a/server/tests/k8s/fixtures/k8s_fixtures.py +++ b/server/tests/k8s/fixtures/k8s_fixtures.py @@ -21,10 +21,10 @@ import pytest -from src.api.schema import CreateSandboxRequest, ImageSpec, ResourceLimits -from src.config import KubernetesRuntimeConfig -from src.services.k8s.client import K8sClient -from src.services.k8s.provider_factory import PROVIDER_TYPE_BATCHSANDBOX +from opensandbox_server.api.schema import CreateSandboxRequest, ImageSpec, ResourceLimits +from opensandbox_server.config import KubernetesRuntimeConfig +from opensandbox_server.services.k8s.client import K8sClient +from opensandbox_server.services.k8s.provider_factory import PROVIDER_TYPE_BATCHSANDBOX @pytest.fixture @@ -205,7 +205,7 @@ def fixed_datetime(): @pytest.fixture def k8s_app_config(k8s_runtime_config): """Provide complete app configuration (Kubernetes type)""" - from src.config import AppConfig, RuntimeConfig, ServerConfig + from opensandbox_server.config import AppConfig, RuntimeConfig, ServerConfig return AppConfig( server=ServerConfig( @@ -225,7 +225,7 @@ def k8s_app_config(k8s_runtime_config): @pytest.fixture def agent_sandbox_app_config(agent_sandbox_runtime_config): """Provide complete app configuration (kubernetes + agent-sandbox provider)""" - from src.config import AppConfig, RuntimeConfig, ServerConfig, AgentSandboxRuntimeConfig + from opensandbox_server.config import AppConfig, RuntimeConfig, ServerConfig, AgentSandboxRuntimeConfig return AppConfig( server=ServerConfig( @@ -250,7 +250,7 @@ def agent_sandbox_app_config(agent_sandbox_runtime_config): @pytest.fixture def app_config_no_k8s(): """Provide app configuration without Kubernetes config""" - from src.config import AppConfig, RuntimeConfig, ServerConfig + from opensandbox_server.config import AppConfig, RuntimeConfig, ServerConfig return AppConfig( server=ServerConfig( @@ -270,7 +270,7 @@ def app_config_no_k8s(): @pytest.fixture def app_config_docker(): """Provide Docker type app configuration""" - from src.config import AppConfig, RuntimeConfig, ServerConfig + from opensandbox_server.config import AppConfig, RuntimeConfig, ServerConfig return AppConfig( server=ServerConfig( @@ -292,8 +292,8 @@ def k8s_service(k8s_app_config): """Provide mocked KubernetesSandboxService""" from unittest.mock import patch, MagicMock - with patch('src.services.k8s.kubernetes_service.K8sClient') as mock_k8s_client_cls, \ - patch('src.services.k8s.kubernetes_service.create_workload_provider') as mock_create_provider: + with patch('opensandbox_server.services.k8s.kubernetes_service.K8sClient') as mock_k8s_client_cls, \ + patch('opensandbox_server.services.k8s.kubernetes_service.create_workload_provider') as mock_create_provider: # Mock K8sClient instance mock_k8s_client = MagicMock() @@ -303,7 +303,7 @@ def k8s_service(k8s_app_config): mock_provider = MagicMock() mock_create_provider.return_value = mock_provider - from src.services.k8s.kubernetes_service import KubernetesSandboxService + from opensandbox_server.services.k8s.kubernetes_service import KubernetesSandboxService service = KubernetesSandboxService(k8s_app_config) # Save mock objects for access in tests @@ -316,7 +316,7 @@ def k8s_service(k8s_app_config): @pytest.fixture def create_sandbox_request(): """Provide standard sandbox creation request""" - from src.api.schema import ResourceLimits + from opensandbox_server.api.schema import ResourceLimits return CreateSandboxRequest( image=ImageSpec(uri="python:3.9"), @@ -361,7 +361,7 @@ def isolated_registry(): Saves the original registry before test and restores it after, preventing global state pollution. """ - from src.services.k8s import provider_factory + from opensandbox_server.services.k8s import provider_factory # Save original registry original_registry = provider_factory._PROVIDER_REGISTRY.copy() diff --git a/server/tests/k8s/test_agent_sandbox_provider.py b/server/tests/k8s/test_agent_sandbox_provider.py index 29ed21c97..68f6b33a6 100644 --- a/server/tests/k8s/test_agent_sandbox_provider.py +++ b/server/tests/k8s/test_agent_sandbox_provider.py @@ -23,8 +23,8 @@ import pytest from kubernetes.client import ApiException -from src.api.schema import ImageSpec, NetworkPolicy, NetworkRule -from src.config import ( +from opensandbox_server.api.schema import ImageSpec, NetworkPolicy, NetworkRule +from opensandbox_server.config import ( AppConfig, AgentSandboxRuntimeConfig, EGRESS_MODE_DNS, @@ -33,9 +33,9 @@ KubernetesRuntimeConfig, RuntimeConfig, ) -from src.services.constants import SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY -from src.services.k8s.agent_sandbox_provider import AgentSandboxProvider -from src.services.constants import OPENSANDBOX_EGRESS_TOKEN +from opensandbox_server.services.constants import SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY +from opensandbox_server.services.k8s.agent_sandbox_provider import AgentSandboxProvider +from opensandbox_server.services.constants import OPENSANDBOX_EGRESS_TOKEN def _app_config(shutdown_policy: str = "Delete", service_account: str | None = None, execd_init_resources: ExecdInitResources | None = None) -> AppConfig: diff --git a/server/tests/k8s/test_agent_sandbox_template.py b/server/tests/k8s/test_agent_sandbox_template.py index 58aa5eeef..5d81f6b58 100644 --- a/server/tests/k8s/test_agent_sandbox_template.py +++ b/server/tests/k8s/test_agent_sandbox_template.py @@ -19,7 +19,7 @@ import pytest import yaml -from src.services.k8s.agent_sandbox_template import AgentSandboxTemplateManager +from opensandbox_server.services.k8s.agent_sandbox_template import AgentSandboxTemplateManager class TestAgentSandboxTemplateManager: diff --git a/server/tests/k8s/test_batchsandbox_provider.py b/server/tests/k8s/test_batchsandbox_provider.py index 4413cef2d..9b2e0e820 100644 --- a/server/tests/k8s/test_batchsandbox_provider.py +++ b/server/tests/k8s/test_batchsandbox_provider.py @@ -21,8 +21,8 @@ from unittest.mock import MagicMock from kubernetes.client import ApiException -from src.api.schema import ImageSpec, ImageAuth, NetworkPolicy, NetworkRule -from src.config import ( +from opensandbox_server.api.schema import ImageSpec, ImageAuth, NetworkPolicy, NetworkRule +from opensandbox_server.config import ( AppConfig, EGRESS_MODE_DNS, EGRESS_MODE_DNS_NFT, @@ -30,11 +30,11 @@ KubernetesRuntimeConfig, RuntimeConfig, ) -from src.services.constants import SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY -from src.services.k8s.batchsandbox_provider import BatchSandboxProvider -from src.services.constants import OPENSANDBOX_EGRESS_TOKEN -from src.services.k8s.image_pull_secret_helper import IMAGE_AUTH_SECRET_PREFIX -from src.services.k8s.volume_helper import apply_volumes_to_pod_spec +from opensandbox_server.services.constants import SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY +from opensandbox_server.services.k8s.batchsandbox_provider import BatchSandboxProvider +from opensandbox_server.services.constants import OPENSANDBOX_EGRESS_TOKEN +from opensandbox_server.services.k8s.image_pull_secret_helper import IMAGE_AUTH_SECRET_PREFIX +from opensandbox_server.services.k8s.volume_helper import apply_volumes_to_pod_spec def _app_config_with_template(template_file_path: str) -> AppConfig: @@ -1720,7 +1720,7 @@ def test_create_workload_with_pvc_volume(self, mock_k8s_client): - Volume mount is added to main container - claimName is correctly set """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC provider = BatchSandboxProvider(mock_k8s_client) mock_k8s_client.create_custom_object.return_value = { @@ -1775,7 +1775,7 @@ def test_create_workload_with_pvc_volume_readonly(self, mock_k8s_client): """ Test creating workload with read-only PVC volume mount. """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC provider = BatchSandboxProvider(mock_k8s_client) mock_k8s_client.create_custom_object.return_value = { @@ -1817,7 +1817,7 @@ def test_create_workload_with_pvc_volume_subpath(self, mock_k8s_client): """ Test creating workload with PVC volume mount with subPath. """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC provider = BatchSandboxProvider(mock_k8s_client) mock_k8s_client.create_custom_object.return_value = { @@ -1860,7 +1860,7 @@ def test_create_workload_with_host_volume(self, mock_k8s_client): """ Test creating workload with hostPath volume mount. """ - from src.api.schema import Volume, Host + from opensandbox_server.api.schema import Volume, Host provider = BatchSandboxProvider(mock_k8s_client) mock_k8s_client.create_custom_object.return_value = { @@ -1911,7 +1911,7 @@ def test_create_workload_with_multiple_volumes(self, mock_k8s_client): """ Test creating workload with multiple volumes (PVC and hostPath). """ - from src.api.schema import Volume, PVC, Host + from opensandbox_server.api.schema import Volume, PVC, Host provider = BatchSandboxProvider(mock_k8s_client) mock_k8s_client.create_custom_object.return_value = { @@ -1964,7 +1964,7 @@ def test_create_workload_pool_mode_rejects_volumes(self, mock_k8s_client): """ Test that pool mode rejects volumes with clear error message. """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC provider = BatchSandboxProvider(mock_k8s_client) @@ -2010,7 +2010,7 @@ def test_apply_volumes_to_pod_spec_no_containers(self, mock_k8s_client): """ Test apply_volumes_to_pod_spec with no containers returns early without error. """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC pod_spec = {"volumes": []} volumes = [Volume(name="test", pvc=PVC(claim_name="pvc"), mount_path="/mnt")] @@ -2025,7 +2025,7 @@ def test_apply_volumes_to_pod_spec_duplicate_internal_volume(self, mock_k8s_clie """ Test apply_volumes_to_pod_spec rejects volume names that collide with internal volumes. """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC pod_spec = { "containers": [{"name": "sandbox", "volumeMounts": []}], @@ -2045,7 +2045,7 @@ def test_apply_volumes_to_pod_spec_same_pvc_multiple_mounts(self, mock_k8s_clien Kubernetes volume is created; multiple volumeMounts reference it (avoids CSI driver issues from duplicate PVC volume definitions). """ - from src.api.schema import Volume, PVC + from opensandbox_server.api.schema import Volume, PVC pod_spec = { "containers": [{"name": "main", "volumeMounts": []}], diff --git a/server/tests/k8s/test_batchsandbox_template.py b/server/tests/k8s/test_batchsandbox_template.py index 37bce3793..468fb0137 100644 --- a/server/tests/k8s/test_batchsandbox_template.py +++ b/server/tests/k8s/test_batchsandbox_template.py @@ -19,7 +19,7 @@ import pytest import yaml -from src.services.k8s.batchsandbox_template import BatchSandboxTemplateManager +from opensandbox_server.services.k8s.batchsandbox_template import BatchSandboxTemplateManager class TestBatchSandboxTemplateManager: diff --git a/server/tests/k8s/test_egress_helper.py b/server/tests/k8s/test_egress_helper.py index 47bcc6081..63fc69259 100644 --- a/server/tests/k8s/test_egress_helper.py +++ b/server/tests/k8s/test_egress_helper.py @@ -19,10 +19,10 @@ import json from typing import Optional -from src.api.schema import NetworkPolicy, NetworkRule -from src.config import EGRESS_MODE_DNS, EGRESS_MODE_DNS_NFT -from src.services.constants import EGRESS_MODE_ENV, EGRESS_RULES_ENV, OPENSANDBOX_EGRESS_TOKEN -from src.services.k8s.egress_helper import ( +from opensandbox_server.api.schema import NetworkPolicy, NetworkRule +from opensandbox_server.config import EGRESS_MODE_DNS, EGRESS_MODE_DNS_NFT +from opensandbox_server.services.constants import EGRESS_MODE_ENV, EGRESS_RULES_ENV, OPENSANDBOX_EGRESS_TOKEN +from opensandbox_server.services.k8s.egress_helper import ( apply_egress_to_spec, build_security_context_for_sandbox_container, prep_execd_init_for_egress, diff --git a/server/tests/k8s/test_image_pull_secret_helper.py b/server/tests/k8s/test_image_pull_secret_helper.py index 88a57a7f7..282b73a4d 100644 --- a/server/tests/k8s/test_image_pull_secret_helper.py +++ b/server/tests/k8s/test_image_pull_secret_helper.py @@ -19,8 +19,8 @@ import base64 import json -from src.api.schema import ImageAuth -from src.services.k8s.image_pull_secret_helper import ( +from opensandbox_server.api.schema import ImageAuth +from opensandbox_server.services.k8s.image_pull_secret_helper import ( IMAGE_AUTH_SECRET_PREFIX, build_image_pull_secret, build_image_pull_secret_name, diff --git a/server/tests/k8s/test_informer.py b/server/tests/k8s/test_informer.py index 7f0876f15..f5d07a906 100644 --- a/server/tests/k8s/test_informer.py +++ b/server/tests/k8s/test_informer.py @@ -17,7 +17,7 @@ import time from unittest.mock import MagicMock -from src.services.k8s.informer import WorkloadInformer +from opensandbox_server.services.k8s.informer import WorkloadInformer def _make_informer(**kwargs) -> WorkloadInformer: diff --git a/server/tests/k8s/test_k8s_client.py b/server/tests/k8s/test_k8s_client.py index be926dccd..76dcd7c22 100644 --- a/server/tests/k8s/test_k8s_client.py +++ b/server/tests/k8s/test_k8s_client.py @@ -21,8 +21,8 @@ from kubernetes.client import ApiException -from src.config import KubernetesRuntimeConfig -from src.services.k8s.client import K8sClient +from opensandbox_server.config import KubernetesRuntimeConfig +from opensandbox_server.services.k8s.client import K8sClient class TestK8sClient: diff --git a/server/tests/k8s/test_kubernetes_service.py b/server/tests/k8s/test_kubernetes_service.py index 8a50871a7..9af058e31 100644 --- a/server/tests/k8s/test_kubernetes_service.py +++ b/server/tests/k8s/test_kubernetes_service.py @@ -21,16 +21,16 @@ from unittest.mock import MagicMock, patch from fastapi import HTTPException -from src.services.k8s.kubernetes_service import KubernetesSandboxService -from src.services.constants import ( +from opensandbox_server.services.k8s.kubernetes_service import KubernetesSandboxService +from opensandbox_server.services.constants import ( OPEN_SANDBOX_EGRESS_AUTH_HEADER, SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY, SANDBOX_MANUAL_CLEANUP_LABEL, SandboxErrorCodes, ) -from src.api.schema import ImageAuth, ListSandboxesRequest, NetworkPolicy -from src.config import EGRESS_MODE_DNS, EGRESS_MODE_DNS_NFT, EgressConfig -from src.api.schema import Endpoint +from opensandbox_server.api.schema import ImageAuth, ListSandboxesRequest, NetworkPolicy +from opensandbox_server.config import EGRESS_MODE_DNS, EGRESS_MODE_DNS_NFT, EgressConfig +from opensandbox_server.api.schema import Endpoint class TestKubernetesSandboxServiceInit: @@ -42,8 +42,8 @@ def test_init_with_valid_config_succeeds(self, k8s_app_config): Purpose: Verify that service can be successfully initialized with valid Kubernetes config """ - with patch('src.services.k8s.kubernetes_service.K8sClient') as mock_k8s_client, \ - patch('src.services.k8s.kubernetes_service.create_workload_provider') as mock_create_provider: + with patch('opensandbox_server.services.k8s.kubernetes_service.K8sClient') as mock_k8s_client, \ + patch('opensandbox_server.services.k8s.kubernetes_service.create_workload_provider') as mock_create_provider: mock_provider = MagicMock() mock_create_provider.return_value = mock_provider @@ -84,7 +84,7 @@ def test_init_with_k8s_client_failure_raises_http_exception(self, k8s_app_config Purpose: Verify that correct HTTPException is raised when K8sClient initialization fails """ - with patch('src.services.k8s.kubernetes_service.K8sClient') as mock_k8s_client: + with patch('opensandbox_server.services.k8s.kubernetes_service.K8sClient') as mock_k8s_client: mock_k8s_client.side_effect = Exception("Failed to load kubeconfig") with pytest.raises(HTTPException) as exc_info: @@ -243,7 +243,7 @@ async def test_create_sandbox_with_network_policy_passes_egress_token_and_annota } with patch( - "src.services.k8s.kubernetes_service.generate_egress_token", + "opensandbox_server.services.k8s.kubernetes_service.generate_egress_token", return_value="egress-token", ): await k8s_service.create_sandbox(create_sandbox_request) @@ -272,7 +272,7 @@ async def test_create_sandbox_with_network_policy_passes_egress_mode_dns_nft_fro } with patch( - "src.services.k8s.kubernetes_service.generate_egress_token", + "opensandbox_server.services.k8s.kubernetes_service.generate_egress_token", return_value="egress-token", ): await k8s_service.create_sandbox(create_sandbox_request) @@ -515,7 +515,7 @@ def test_list_all_sandboxes_succeeds(self, k8s_service, mock_workload): k8s_service.workload_provider.get_endpoint_info.return_value = "10.0.0.1:8080" k8s_service.workload_provider.get_expiration.return_value = datetime.now(timezone.utc) + timedelta(hours=1) - from src.api.schema import PaginationRequest + from opensandbox_server.api.schema import PaginationRequest request = ListSandboxesRequest(pagination=PaginationRequest(page=1, page_size=20)) response = k8s_service.list_sandboxes(request) @@ -558,7 +558,7 @@ def test_list_sandboxes_with_pagination(self, k8s_service, mock_workload): k8s_service.workload_provider.get_endpoint_info.return_value = "10.0.0.1:8080" k8s_service.workload_provider.get_expiration.return_value = datetime.now(timezone.utc) + timedelta(hours=1) - from src.api.schema import PaginationRequest + from opensandbox_server.api.schema import PaginationRequest request = ListSandboxesRequest(pagination=PaginationRequest(page=1, page_size=5)) response = k8s_service.list_sandboxes(request) @@ -614,7 +614,7 @@ def test_list_sandboxes_sorted_by_creation_time(self, k8s_service, mock_workload k8s_service.workload_provider.get_endpoint_info.return_value = "10.0.0.1:8080" k8s_service.workload_provider.get_expiration.return_value = datetime.now(timezone.utc) + timedelta(hours=1) - from src.api.schema import PaginationRequest + from opensandbox_server.api.schema import PaginationRequest request = ListSandboxesRequest(pagination=PaginationRequest(page=1, page_size=10)) response = k8s_service.list_sandboxes(request) @@ -649,7 +649,7 @@ def test_renew_expiration_succeeds(self, k8s_service, mock_workload): k8s_service.workload_provider.update_expiration.return_value = None k8s_service.workload_provider.get_expiration.return_value = new_expiration - from src.api.schema import RenewSandboxExpirationRequest + from opensandbox_server.api.schema import RenewSandboxExpirationRequest request = RenewSandboxExpirationRequest(expires_at=new_expiration) response = k8s_service.renew_expiration("test-sandbox-id", request) @@ -671,7 +671,7 @@ def test_renew_with_past_time_raises_error(self, k8s_service, mock_workload): k8s_service.workload_provider.get_workload.return_value = mock_workload - from src.api.schema import RenewSandboxExpirationRequest + from opensandbox_server.api.schema import RenewSandboxExpirationRequest request = RenewSandboxExpirationRequest(expires_at=past_time) with pytest.raises(HTTPException) as exc_info: @@ -683,7 +683,7 @@ def test_renew_returns_409_when_sandbox_has_no_expiration(self, k8s_service): """Renew is rejected with 409 when sandbox has no TTL (manual cleanup).""" k8s_service.workload_provider.get_workload.return_value = MagicMock() k8s_service.workload_provider.get_expiration.return_value = None - from src.api.schema import RenewSandboxExpirationRequest + from opensandbox_server.api.schema import RenewSandboxExpirationRequest request = RenewSandboxExpirationRequest( expires_at=datetime.now(timezone.utc) + timedelta(hours=1) ) diff --git a/server/tests/k8s/test_pool_service.py b/server/tests/k8s/test_pool_service.py index 366cee109..dffd84780 100644 --- a/server/tests/k8s/test_pool_service.py +++ b/server/tests/k8s/test_pool_service.py @@ -13,7 +13,7 @@ # limitations under the License. """ -Unit tests for PoolService (server/src/services/k8s/pool_service.py). +Unit tests for PoolService (server/opensandbox_server/services/k8s/pool_service.py). All tests mock the Kubernetes CustomObjectsApi so no cluster connection is needed. """ @@ -22,13 +22,13 @@ from unittest.mock import MagicMock from kubernetes.client import ApiException -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreatePoolRequest, PoolCapacitySpec, UpdatePoolRequest, ) -from src.services.constants import SandboxErrorCodes -from src.services.k8s.pool_service import PoolService +from opensandbox_server.services.constants import SandboxErrorCodes +from opensandbox_server.services.k8s.pool_service import PoolService from fastapi import HTTPException diff --git a/server/tests/k8s/test_provider_factory.py b/server/tests/k8s/test_provider_factory.py index 20c938908..e6032f1e4 100644 --- a/server/tests/k8s/test_provider_factory.py +++ b/server/tests/k8s/test_provider_factory.py @@ -19,17 +19,17 @@ import pytest from unittest.mock import patch -from src.config import AgentSandboxRuntimeConfig -from src.services.k8s.provider_factory import ( +from opensandbox_server.config import AgentSandboxRuntimeConfig +from opensandbox_server.services.k8s.provider_factory import ( register_provider, create_workload_provider, list_available_providers, PROVIDER_TYPE_BATCHSANDBOX, PROVIDER_TYPE_AGENT_SANDBOX, ) -from src.services.k8s.workload_provider import WorkloadProvider -from src.services.k8s.batchsandbox_provider import BatchSandboxProvider -from src.services.k8s.agent_sandbox_provider import AgentSandboxProvider +from opensandbox_server.services.k8s.workload_provider import WorkloadProvider +from opensandbox_server.services.k8s.batchsandbox_provider import BatchSandboxProvider +from opensandbox_server.services.k8s.agent_sandbox_provider import AgentSandboxProvider @@ -225,7 +225,7 @@ def test_create_provider_with_empty_registry_raises_error(self, mock_k8s_client, Purpose: Verify that ValueError is raised when no provider is registered and type is None """ - from src.services.k8s import provider_factory + from opensandbox_server.services.k8s import provider_factory # Clear the registry to test empty registry scenario provider_factory._PROVIDER_REGISTRY.clear() diff --git a/server/tests/k8s/test_rate_limiter.py b/server/tests/k8s/test_rate_limiter.py index 6dbf005bc..8f18181ae 100644 --- a/server/tests/k8s/test_rate_limiter.py +++ b/server/tests/k8s/test_rate_limiter.py @@ -20,7 +20,7 @@ import pytest -from src.services.k8s.rate_limiter import TokenBucketRateLimiter +from opensandbox_server.services.k8s.rate_limiter import TokenBucketRateLimiter class TestTokenBucketRateLimiter: @@ -113,7 +113,7 @@ def test_acquire_minimum_sleep_prevents_busy_loop(self): with limiter._lock: limiter._tokens = 1.0 - 1e-10 - with patch("src.services.k8s.rate_limiter.time.sleep") as mock_sleep: + with patch("opensandbox_server.services.k8s.rate_limiter.time.sleep") as mock_sleep: # _try_acquire will succeed on first or second call; we only care # that if sleep is called, the argument is >= 0.001. limiter.acquire() @@ -160,7 +160,7 @@ def test_concurrent_acquires_do_not_exceed_burst(self): fixed_time = limiter._last_refill def worker(): - with patch("src.services.k8s.rate_limiter.time.monotonic", return_value=fixed_time): + with patch("opensandbox_server.services.k8s.rate_limiter.time.monotonic", return_value=fixed_time): if limiter.try_acquire(): with lock: successes.append(1) diff --git a/server/tests/test_agent_sandbox_service.py b/server/tests/test_agent_sandbox_service.py index f2f52006a..979f1ddb5 100644 --- a/server/tests/test_agent_sandbox_service.py +++ b/server/tests/test_agent_sandbox_service.py @@ -23,16 +23,16 @@ from fastapi import HTTPException from pydantic import ValidationError -from src.api.schema import SandboxStatus -from src.config import ( +from opensandbox_server.api.schema import SandboxStatus +from opensandbox_server.config import ( AppConfig, RuntimeConfig, ServerConfig, KubernetesRuntimeConfig, AgentSandboxRuntimeConfig, ) -from src.services.k8s.kubernetes_service import KubernetesSandboxService -from src.services.constants import SandboxErrorCodes +from opensandbox_server.services.k8s.kubernetes_service import KubernetesSandboxService +from opensandbox_server.services.constants import SandboxErrorCodes @pytest.fixture @@ -113,8 +113,8 @@ def test_init_with_valid_config_succeeds(self, agent_sandbox_runtime_config): ), ) - with patch("src.services.k8s.kubernetes_service.K8sClient") as mock_k8s_client, patch( - "src.services.k8s.kubernetes_service.create_workload_provider" + with patch("opensandbox_server.services.k8s.kubernetes_service.K8sClient") as mock_k8s_client, patch( + "opensandbox_server.services.k8s.kubernetes_service.create_workload_provider" ) as mock_provider_factory: mock_provider_factory.return_value = MagicMock() @@ -162,7 +162,7 @@ def test_init_with_k8s_client_failure_raises_http_exception(self, agent_sandbox_ """ Test case: Raises HTTPException when K8sClient initialization fails """ - with patch("src.services.k8s.kubernetes_service.K8sClient") as mock_k8s_client: + with patch("opensandbox_server.services.k8s.kubernetes_service.K8sClient") as mock_k8s_client: mock_k8s_client.side_effect = Exception("Failed to load kubeconfig") with pytest.raises(HTTPException) as exc_info: diff --git a/server/tests/test_auth_middleware.py b/server/tests/test_auth_middleware.py index bf7b49bae..1e29539b0 100644 --- a/server/tests/test_auth_middleware.py +++ b/server/tests/test_auth_middleware.py @@ -15,8 +15,8 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -from src.config import AppConfig, IngressConfig, RuntimeConfig, ServerConfig -from src.middleware.auth import AuthMiddleware +from opensandbox_server.config import AppConfig, IngressConfig, RuntimeConfig, ServerConfig +from opensandbox_server.middleware.auth import AuthMiddleware def _app_config_with_api_key() -> AppConfig: diff --git a/server/tests/test_config.py b/server/tests/test_config.py index 7d46638c5..80922d3c1 100644 --- a/server/tests/test_config.py +++ b/server/tests/test_config.py @@ -17,8 +17,8 @@ import pytest from pydantic import ValidationError -from src import config as config_module -from src.config import ( +from opensandbox_server import config as config_module +from opensandbox_server.config import ( AppConfig, RenewIntentRedisConfig, EGRESS_MODE_DNS, diff --git a/server/tests/test_docker_endpoint.py b/server/tests/test_docker_endpoint.py index 353086144..54bd6016e 100644 --- a/server/tests/test_docker_endpoint.py +++ b/server/tests/test_docker_endpoint.py @@ -15,13 +15,13 @@ import pytest from unittest.mock import MagicMock, patch -from src.services.constants import ( +from opensandbox_server.services.constants import ( OPEN_SANDBOX_EGRESS_AUTH_HEADER, SANDBOX_EMBEDDING_PROXY_PORT_LABEL, SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY, ) -from src.services.docker import DockerSandboxService -from src.config import AppConfig, RuntimeConfig, DockerConfig, ServerConfig +from opensandbox_server.services.docker import DockerSandboxService +from opensandbox_server.config import AppConfig, RuntimeConfig, DockerConfig, ServerConfig @pytest.fixture def mock_docker_service(): @@ -54,7 +54,7 @@ def test_get_endpoint_host_mode(mock_docker_service): mock_container.attrs = {"State": {"Running": True}} mock_client.containers.list.return_value = [mock_container] - with patch("src.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="10.0.0.1"): + 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" @@ -79,7 +79,7 @@ def test_get_endpoint_bridge_http_port(mock_docker_service): } mock_client.containers.list.return_value = [mock_container] - with patch("src.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"): + 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" @@ -102,7 +102,7 @@ def test_get_endpoint_bridge_other_port_via_execd(mock_docker_service): } mock_client.containers.list.return_value = [mock_container] - with patch("src.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"): + 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" @@ -126,7 +126,7 @@ def test_get_endpoint_bridge_egress_port_includes_auth_header(mock_docker_servic } mock_client.containers.list.return_value = [mock_container] - with patch("src.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"): + 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" @@ -152,7 +152,7 @@ def test_get_endpoint_bridge_non_egress_port_still_includes_instance_auth_header } mock_client.containers.list.return_value = [mock_container] - with patch("src.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="192.168.1.100"): + 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" @@ -277,7 +277,7 @@ def test_get_endpoint_bridge_uses_docker_host_ip_when_server_in_container(): } mock_client.containers.list.return_value = [mock_container] - with patch("src.services.docker._running_inside_docker_container", return_value=True): + with patch("opensandbox_server.services.docker._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" @@ -308,7 +308,7 @@ def test_get_endpoint_user_defined_network_external(mock_docker_service): } mock_client.containers.list.return_value = [mock_container] - with patch("src.services.sandbox_service.SandboxService._resolve_bind_ip", return_value="10.0.1.1"): + 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) diff --git a/server/tests/test_docker_path_fix.py b/server/tests/test_docker_path_fix.py index a6acd1232..7473456a6 100644 --- a/server/tests/test_docker_path_fix.py +++ b/server/tests/test_docker_path_fix.py @@ -14,8 +14,8 @@ import posixpath from unittest.mock import MagicMock, patch -from src.services.docker import DockerSandboxService, EXECED_INSTALL_PATH, BOOTSTRAP_PATH -from src.config import AppConfig, RuntimeConfig, ServerConfig +from opensandbox_server.services.docker import DockerSandboxService, EXECED_INSTALL_PATH, BOOTSTRAP_PATH +from opensandbox_server.config import AppConfig, RuntimeConfig, ServerConfig def _app_config() -> AppConfig: return AppConfig( @@ -32,7 +32,7 @@ def test_container_internal_paths_use_posix_style(): assert EXECED_INSTALL_PATH == "/opt/opensandbox/execd" assert BOOTSTRAP_PATH == "/opt/opensandbox/bootstrap.sh" -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_copy_execd_to_container_uses_posix_dirname(mock_docker): """Verify _copy_execd_to_container uses posixpath for target directory.""" service = DockerSandboxService(config=_app_config()) @@ -49,7 +49,7 @@ def test_copy_execd_to_container_uses_posix_dirname(mock_docker): expected_parent = posixpath.dirname(EXECED_INSTALL_PATH.rstrip("/")) or "/" mock_ensure_dir.assert_called_once_with(mock_container, expected_parent, "test-sandbox") -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_install_bootstrap_script_uses_posix_dirname(mock_docker): """Verify _install_bootstrap_script uses posixpath for script directory.""" service = DockerSandboxService(config=_app_config()) diff --git a/server/tests/test_docker_service.py b/server/tests/test_docker_service.py index 57b9deb1c..1249dfb2e 100644 --- a/server/tests/test_docker_service.py +++ b/server/tests/test_docker_service.py @@ -22,7 +22,7 @@ from fastapi import HTTPException, status from pydantic import ValidationError -from src.config import ( +from opensandbox_server.config import ( AppConfig, EGRESS_MODE_DNS, EgressConfig, @@ -31,9 +31,9 @@ StorageConfig, IngressConfig, ) -from src.extensions import ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY -from src.services.constants import EGRESS_MODE_ENV, OPENSANDBOX_EGRESS_TOKEN -from src.services.constants import ( +from opensandbox_server.extensions import ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY +from opensandbox_server.services.constants import EGRESS_MODE_ENV, OPENSANDBOX_EGRESS_TOKEN +from opensandbox_server.services.constants import ( SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY, SANDBOX_EXPIRES_AT_LABEL, SANDBOX_ID_LABEL, @@ -41,9 +41,9 @@ SANDBOX_OSSFS_MOUNTS_LABEL, SandboxErrorCodes, ) -from src.services.docker import DockerSandboxService, PendingSandbox -from src.services.helpers import parse_memory_limit, parse_nano_cpus, parse_timestamp -from src.api.schema import ( +from opensandbox_server.services.docker import DockerSandboxService, PendingSandbox +from opensandbox_server.services.helpers import parse_memory_limit, parse_nano_cpus, parse_timestamp +from opensandbox_server.api.schema import ( CreateSandboxRequest, CreateSandboxResponse, Host, @@ -115,7 +115,7 @@ def test_env_allows_empty_string_and_skips_none(): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_applies_security_defaults(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -141,6 +141,7 @@ async def test_create_sandbox_applies_security_defaults(mock_docker): with ( patch.object(service, "_ensure_image_available"), patch.object(service, "_prepare_sandbox_runtime"), + patch.object(service, "_allocate_distinct_host_ports", return_value=(40001, 40002)), ): await service.create_sandbox(request) @@ -169,7 +170,7 @@ async def test_create_sandbox_applies_security_defaults(mock_docker): ], ) @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_prepare_runtime_failure_triggers_cleanup( mock_docker, runtime_exc, expected_status, expect_wrapped_error ): @@ -208,7 +209,7 @@ async def test_prepare_runtime_failure_triggers_cleanup( @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_rejects_invalid_metadata(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -234,7 +235,7 @@ async def test_create_sandbox_rejects_invalid_metadata(mock_docker): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_rejects_timeout_above_configured_maximum(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -262,7 +263,7 @@ async def test_create_sandbox_rejects_timeout_above_configured_maximum(mock_dock @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_requires_entrypoint(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -289,7 +290,7 @@ async def test_create_sandbox_requires_entrypoint(mock_docker): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_network_policy_rejected_on_host_mode(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -318,7 +319,7 @@ async def test_network_policy_rejected_on_host_mode(mock_docker): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_network_policy_requires_egress_image(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -347,7 +348,7 @@ async def test_network_policy_requires_egress_image(mock_docker): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_egress_sidecar_injection_and_capabilities(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -379,7 +380,7 @@ def host_cfg_side_effect(**kwargs): ) with ( - patch("src.services.docker.generate_egress_token", return_value="egress-token"), + patch("opensandbox_server.services.docker.generate_egress_token", return_value="egress-token"), patch.object(service, "_allocate_distinct_host_ports", return_value=(44772, 8080)), patch.object(service, "_ensure_image_available"), patch.object(service, "_prepare_sandbox_runtime"), @@ -419,7 +420,7 @@ def host_cfg_side_effect(**kwargs): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_network_policy_rejected_on_user_defined_network(mock_docker): """networkPolicy must be rejected when network_mode is a user-defined named network.""" mock_client = MagicMock() @@ -450,7 +451,7 @@ async def test_network_policy_rejected_on_user_defined_network(mock_docker): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_fails_when_user_defined_network_not_found(mock_docker): """create_sandbox raises 400 with a clear message when the named network does not exist.""" from docker.errors import NotFound as DockerNotFound @@ -483,7 +484,7 @@ async def test_create_sandbox_fails_when_user_defined_network_not_found(mock_doc @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_user_defined_network_uses_correct_network_mode(mock_docker): """Containers created on a user-defined network use the network name as network_mode.""" @@ -514,6 +515,7 @@ def host_cfg_side_effect(**kwargs): with ( patch.object(service, "_ensure_image_available"), patch.object(service, "_prepare_sandbox_runtime"), + patch.object(service, "_allocate_distinct_host_ports", return_value=(40001, 40002)), ): await service.create_sandbox(request) @@ -521,7 +523,7 @@ def host_cfg_side_effect(**kwargs): assert call_kwargs["host_config"]["network_mode"] == "my-app-net" -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_validate_network_skipped_for_builtin_modes(mock_docker): """_validate_network_exists does NOT call the Docker API for host or bridge modes.""" mock_client = MagicMock() @@ -537,7 +539,7 @@ def test_validate_network_skipped_for_builtin_modes(mock_docker): mock_client.networks.get.assert_not_called() -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_egress_sidecar_cleanup_uses_api_remove_when_lookup_fails(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -579,7 +581,7 @@ def host_cfg_side_effect(**kwargs): mock_client.api.remove_container.assert_called_once_with("sidecar-id", force=True) -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_egress_sidecar_missing_id_preserves_specific_error(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -621,7 +623,7 @@ def host_cfg_side_effect(**kwargs): mock_client.api.remove_container.assert_not_called() -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_egress_sidecar_cleanup_wraps_unexpected_lookup_error(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -753,7 +755,7 @@ def test_build_labels_stores_extensions_json(): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_with_manual_cleanup_completes_full_create_path(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -795,7 +797,7 @@ def test_restore_existing_sandboxes_ignores_manual_cleanup_without_warning(): with ( patch.object(service.docker_client.containers, "list", return_value=[manual_container]), - patch("src.services.docker.logger.warning") as mock_warning, + patch("opensandbox_server.services.docker.logger.warning") as mock_warning, patch.object(service, "_schedule_expiration") as mock_schedule, ): service._restore_existing_sandboxes() @@ -826,7 +828,7 @@ def test_renew_expiration_rejects_manual_cleanup_sandbox(): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_create_sandbox_async_returns_provisioning(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -865,7 +867,7 @@ async def test_create_sandbox_async_returns_provisioning(mock_docker): @pytest.mark.asyncio -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") async def test_get_sandbox_returns_pending_state(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -902,7 +904,7 @@ async def test_get_sandbox_returns_pending_state(mock_docker): assert response.entrypoint == ["python", "app.py"] -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_list_sandboxes_deduplicates_container_and_pending(mock_docker): # Build a realistic container mock to avoid parse_timestamp errors. container = MagicMock() @@ -950,7 +952,7 @@ def test_list_sandboxes_deduplicates_container_and_pending(mock_docker): assert response.items[0].metadata == {"team": "c"} -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_get_sandbox_prefers_container_over_pending(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -995,7 +997,7 @@ def test_get_sandbox_prefers_container_over_pending(mock_docker): assert sandbox.entrypoint == ["/bin/sh"] -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") def test_async_worker_cleans_up_leftover_container_on_failure(mock_docker): mock_client = MagicMock() mock_client.containers.list.return_value = [] @@ -1043,7 +1045,7 @@ def test_async_worker_cleans_up_leftover_container_on_failure(mock_docker): # ============================================================================ -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") class TestBuildVolumeBinds: """Tests for DockerSandboxService._build_volume_binds instance method.""" @@ -1234,7 +1236,7 @@ def test_ossfs_volume_with_subpath(self, mock_docker): assert binds == ["/mnt/ossfs/bucket-test-3/task-001:/mnt/data:rw"] -@patch("src.services.docker.docker") +@patch("opensandbox_server.services.docker.docker") class TestDockerVolumeValidation: """Tests for volume validation in DockerSandboxService.create_sandbox.""" @@ -1314,10 +1316,10 @@ async def test_ossfs_mount_failure_rejected(self, mock_docker): ], ) - with patch("src.services.ossfs_mixin.os.name", "posix"): - with patch("src.services.ossfs_mixin.os.path.ismount", return_value=False): - with patch("src.services.ossfs_mixin.os.makedirs"): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.name", "posix"): + with patch("opensandbox_server.services.ossfs_mixin.os.path.ismount", return_value=False): + with patch("opensandbox_server.services.ossfs_mixin.os.makedirs"): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=1, stderr="mount failed") with pytest.raises(HTTPException) as exc_info: await service.create_sandbox(request) @@ -1342,7 +1344,7 @@ def test_ossfs_windows_host_not_supported(self, mock_docker): mount_path="/mnt/data", ) - with patch("src.services.ossfs_mixin.os.name", "nt"): + with patch("opensandbox_server.services.ossfs_mixin.os.name", "nt"): with pytest.raises(HTTPException) as exc_info: service._validate_ossfs_volume(volume) assert exc_info.value.status_code == status.HTTP_400_BAD_REQUEST @@ -1369,8 +1371,8 @@ def test_ossfs_v1_mount_command_uses_o_options(self, mock_docker): ) backend_path = "/mnt/ossfs/bucket-test-3/task-001" - with patch("src.services.ossfs_mixin.os.makedirs"): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.makedirs"): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0, stderr="") service._mount_ossfs_backend_path(volume, backend_path) @@ -1404,8 +1406,8 @@ def test_ossfs_v2_mount_command_uses_config_file(self, mock_docker): ) backend_path = "/mnt/ossfs/bucket-test-3/task-001" - with patch("src.services.ossfs_mixin.os.makedirs"): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.makedirs"): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0, stderr="") service._mount_ossfs_backend_path(volume, backend_path) @@ -1483,10 +1485,10 @@ async def test_ossfs_volume_binds_passed_to_docker(self, mock_docker): ], ) - with patch("src.services.ossfs_mixin.os.name", "posix"): - with patch("src.services.ossfs_mixin.os.path.ismount", return_value=False): - with patch("src.services.ossfs_mixin.os.makedirs"): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.name", "posix"): + with patch("opensandbox_server.services.ossfs_mixin.os.path.ismount", return_value=False): + with patch("opensandbox_server.services.ossfs_mixin.os.makedirs"): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0, stderr="") with patch.object(service, "_ensure_image_available"), patch.object( service, "_prepare_sandbox_runtime" @@ -1532,9 +1534,9 @@ def test_prepare_ossfs_mounts_reuses_mount_key(self, mock_docker): ), ] - with patch("src.services.ossfs_mixin.os.path.ismount", return_value=False): - with patch("src.services.ossfs_mixin.os.makedirs"): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.path.ismount", return_value=False): + with patch("opensandbox_server.services.ossfs_mixin.os.makedirs"): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0, stderr="") mount_keys = service._prepare_ossfs_mounts(volumes) @@ -1606,8 +1608,8 @@ def test_delete_sandbox_releases_ossfs_mount(self, mock_docker): service = DockerSandboxService(config=_app_config()) service._ossfs_mount_ref_counts[mount_key] = 1 - with patch("src.services.ossfs_mixin.os.path.ismount", return_value=True): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.path.ismount", return_value=True): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0, stderr="") service.delete_sandbox("sandbox-1") @@ -1620,8 +1622,8 @@ def test_release_ossfs_mount_untracked_key_does_not_unmount(self, mock_docker): mock_docker.from_env.return_value = MagicMock() service = DockerSandboxService(config=_app_config()) - with patch("src.services.ossfs_mixin.os.path.ismount", return_value=True): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.path.ismount", return_value=True): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: service._release_ossfs_mount(mount_key) mock_run.assert_not_called() @@ -1683,8 +1685,8 @@ def test_delete_one_sandbox_after_restart_keeps_shared_mount(self, mock_docker): service = DockerSandboxService(config=_app_config()) assert service._ossfs_mount_ref_counts[mount_key] == 2 - with patch("src.services.ossfs_mixin.os.path.ismount", return_value=True): - with patch("src.services.ossfs_mixin.subprocess.run") as mock_run: + with patch("opensandbox_server.services.ossfs_mixin.os.path.ismount", return_value=True): + with patch("opensandbox_server.services.ossfs_mixin.subprocess.run") as mock_run: service.delete_sandbox("sandbox-a") assert service._ossfs_mount_ref_counts[mount_key] == 1 @@ -1899,7 +1901,7 @@ async def test_pvc_subpath_symlink_escape_rejected(self, mock_docker): # Simulate: realpath resolves a symlink that escapes the mountpoint. # datasets -> / inside the volume, so realpath(…/_data/datasets) = / - with patch("src.services.docker.os.path.realpath") as mock_realpath: + with patch("opensandbox_server.services.docker.os.path.realpath") as mock_realpath: mock_realpath.side_effect = lambda p, **kwargs: ("/" if p.endswith("datasets") else p) with pytest.raises(HTTPException) as exc_info: await service.create_sandbox(request) @@ -1980,7 +1982,7 @@ async def test_host_path_not_found_rejected(self, mock_docker): ], ) - with patch("src.services.docker.os.makedirs", side_effect=PermissionError("denied")): + with patch("opensandbox_server.services.docker.os.makedirs", side_effect=PermissionError("denied")): with pytest.raises(HTTPException) as exc_info: await service.create_sandbox(request) diff --git a/server/tests/test_endpoint.py b/server/tests/test_endpoint.py index d121b0cd8..f290ea5f7 100644 --- a/server/tests/test_endpoint.py +++ b/server/tests/test_endpoint.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from src.services.helpers import normalize_external_endpoint_url +from opensandbox_server.services.helpers import normalize_external_endpoint_url def test_normalize_external_endpoint_url_defaults_to_https() -> None: diff --git a/server/tests/test_endpoint_auth.py b/server/tests/test_endpoint_auth.py index 97c89188a..ecf173e58 100644 --- a/server/tests/test_endpoint_auth.py +++ b/server/tests/test_endpoint_auth.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from src.services.constants import OPEN_SANDBOX_EGRESS_AUTH_HEADER -from src.services.endpoint_auth import ( +from opensandbox_server.services.constants import OPEN_SANDBOX_EGRESS_AUTH_HEADER +from opensandbox_server.services.endpoint_auth import ( build_egress_auth_headers, generate_egress_token, merge_endpoint_headers, diff --git a/server/tests/test_extensions.py b/server/tests/test_extensions.py index b1703c92b..fddf4f385 100644 --- a/server/tests/test_extensions.py +++ b/server/tests/test_extensions.py @@ -15,7 +15,7 @@ import pytest from fastapi import HTTPException -from src.extensions import ( +from opensandbox_server.extensions import ( ACCESS_RENEW_EXTEND_SECONDS_KEY, ACCESS_RENEW_EXTEND_SECONDS_MAX, ACCESS_RENEW_EXTEND_SECONDS_METADATA_KEY, diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index 4e93896ce..a312b4cc9 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -14,7 +14,7 @@ from datetime import datetime, timezone -from src.services.helpers import parse_timestamp +from opensandbox_server.services.helpers import parse_timestamp def test_parse_timestamp_truncates_nanoseconds(): diff --git a/server/tests/test_ingress.py b/server/tests/test_ingress.py index 00c23f93d..3c3109c19 100644 --- a/server/tests/test_ingress.py +++ b/server/tests/test_ingress.py @@ -13,15 +13,15 @@ # limitations under the License. -from src.config import ( +from opensandbox_server.config import ( GatewayConfig, GatewayRouteModeConfig, IngressConfig, INGRESS_MODE_DIRECT, INGRESS_MODE_GATEWAY, ) -from src.services.constants import OPEN_SANDBOX_INGRESS_HEADER -from src.services.helpers import format_ingress_endpoint +from opensandbox_server.services.constants import OPEN_SANDBOX_INGRESS_HEADER +from opensandbox_server.services.helpers import format_ingress_endpoint def test_format_ingress_endpoint_returns_none_when_not_gateway(): diff --git a/server/tests/test_integrations_redis.py b/server/tests/test_integrations_redis.py index b970ad1b5..682bef4f7 100644 --- a/server/tests/test_integrations_redis.py +++ b/server/tests/test_integrations_redis.py @@ -16,8 +16,8 @@ import pytest -from src.config import AppConfig, RenewIntentConfig, RenewIntentRedisConfig, RuntimeConfig, ServerConfig -from src.integrations.renew_intent.redis_client import ( +from opensandbox_server.config import AppConfig, RenewIntentConfig, RenewIntentRedisConfig, RuntimeConfig, ServerConfig +from opensandbox_server.integrations.renew_intent.redis_client import ( close_renew_intent_redis_client, connect_renew_intent_redis_from_config, ) @@ -52,7 +52,7 @@ async def test_connect_returns_none_when_redis_disabled(): @pytest.mark.asyncio -@patch("src.integrations.renew_intent.redis_client.redis_async") +@patch("opensandbox_server.integrations.renew_intent.redis_client.redis_async") async def test_connect_pings_when_enabled(mock_redis_mod): mock_client = AsyncMock() mock_redis_mod.from_url.return_value = mock_client diff --git a/server/tests/test_pool_api.py b/server/tests/test_pool_api.py index 807fc78ee..5de3190f8 100644 --- a/server/tests/test_pool_api.py +++ b/server/tests/test_pool_api.py @@ -13,7 +13,7 @@ # limitations under the License. """ -Integration-style tests for Pool API routes (src/api/pool.py). +Integration-style tests for Pool API routes (opensandbox_server/api/pool.py). Routes are exercised via FastAPI TestClient. The K8s PoolService is patched so no real cluster connection is needed. @@ -23,7 +23,7 @@ from fastapi.testclient import TestClient from fastapi import HTTPException, status as http_status -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreatePoolRequest, ListPoolsResponse, PoolCapacitySpec, @@ -31,14 +31,14 @@ PoolStatus, UpdatePoolRequest, ) -from src.services.constants import SandboxErrorCodes +from opensandbox_server.services.constants import SandboxErrorCodes # --------------------------------------------------------------------------- # Shared helpers # --------------------------------------------------------------------------- -_POOL_SERVICE_PATCH = "src.api.pool._get_pool_service" +_POOL_SERVICE_PATCH = "opensandbox_server.api.pool._get_pool_service" def _cap(buffer_max=3, buffer_min=1, pool_max=10, pool_min=0) -> PoolCapacitySpec: diff --git a/server/tests/test_proxy_renew_coordinator.py b/server/tests/test_proxy_renew_coordinator.py index e7306e35d..689b05472 100644 --- a/server/tests/test_proxy_renew_coordinator.py +++ b/server/tests/test_proxy_renew_coordinator.py @@ -17,10 +17,10 @@ import pytest -from src.config import AppConfig, RenewIntentConfig, RuntimeConfig, ServerConfig -from src.integrations.renew_intent.consumer import RenewIntentConsumer, RenewWorkItem -from src.integrations.renew_intent.logutil import RENEW_SOURCE_SERVER_PROXY -from src.integrations.renew_intent.proxy_renew import ProxyRenewCoordinator +from opensandbox_server.config import AppConfig, RenewIntentConfig, RuntimeConfig, ServerConfig +from opensandbox_server.integrations.renew_intent.consumer import RenewIntentConsumer, RenewWorkItem +from opensandbox_server.integrations.renew_intent.logutil import RENEW_SOURCE_SERVER_PROXY +from opensandbox_server.integrations.renew_intent.proxy_renew import ProxyRenewCoordinator def _app_config(*, renew_enabled: bool = True, min_interval: int = 60) -> AppConfig: @@ -80,7 +80,7 @@ def mono(): return next(seq, 999.0) monkeypatch.setattr( - "src.integrations.renew_intent.consumer.time.monotonic", + "opensandbox_server.integrations.renew_intent.consumer.time.monotonic", mono, ) @@ -112,7 +112,7 @@ def mono(): return next(seq, 999.0) monkeypatch.setattr( - "src.integrations.renew_intent.consumer.time.monotonic", + "opensandbox_server.integrations.renew_intent.consumer.time.monotonic", mono, ) diff --git a/server/tests/test_renew_intent.py b/server/tests/test_renew_intent.py index 64f617b68..ea2e8815c 100644 --- a/server/tests/test_renew_intent.py +++ b/server/tests/test_renew_intent.py @@ -18,8 +18,8 @@ import pytest -from src.integrations.renew_intent.intent import parse_renew_intent_json -from src.integrations.renew_intent.consumer import RenewIntentConsumer +from opensandbox_server.integrations.renew_intent.intent import parse_renew_intent_json +from opensandbox_server.integrations.renew_intent.consumer import RenewIntentConsumer def test_parse_matches_ingress_intent_shape(): diff --git a/server/tests/test_routes.py b/server/tests/test_routes.py index b06926c25..72660862d 100644 --- a/server/tests/test_routes.py +++ b/server/tests/test_routes.py @@ -23,8 +23,8 @@ from fastapi.testclient import TestClient -from src.api import lifecycle -from src.api.schema import ImageSpec, Sandbox, SandboxStatus +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import ImageSpec, Sandbox, SandboxStatus class TestHealthCheck: diff --git a/server/tests/test_routes_create_delete.py b/server/tests/test_routes_create_delete.py index 5c52468c5..170e57caa 100644 --- a/server/tests/test_routes_create_delete.py +++ b/server/tests/test_routes_create_delete.py @@ -16,8 +16,8 @@ from fastapi.testclient import TestClient -from src.api import lifecycle -from src.api.schema import CreateSandboxResponse, SandboxStatus +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import CreateSandboxResponse, SandboxStatus def test_create_sandbox_returns_202_and_service_payload( diff --git a/server/tests/test_routes_endpoint_behavior.py b/server/tests/test_routes_endpoint_behavior.py index 3bde1038f..83367e8c8 100644 --- a/server/tests/test_routes_endpoint_behavior.py +++ b/server/tests/test_routes_endpoint_behavior.py @@ -14,8 +14,8 @@ from fastapi.testclient import TestClient -from src.api import lifecycle -from src.api.schema import Endpoint +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import Endpoint def test_get_endpoint_returns_service_result( diff --git a/server/tests/test_routes_get_sandbox.py b/server/tests/test_routes_get_sandbox.py index 7585bb8c8..d1079da06 100644 --- a/server/tests/test_routes_get_sandbox.py +++ b/server/tests/test_routes_get_sandbox.py @@ -17,8 +17,8 @@ from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient -from src.api import lifecycle -from src.api.schema import ImageSpec, Sandbox, SandboxStatus +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import ImageSpec, Sandbox, SandboxStatus def test_get_sandbox_returns_service_payload( diff --git a/server/tests/test_routes_list_sandboxes.py b/server/tests/test_routes_list_sandboxes.py index ed3757704..0474ffee9 100644 --- a/server/tests/test_routes_list_sandboxes.py +++ b/server/tests/test_routes_list_sandboxes.py @@ -16,8 +16,8 @@ from fastapi.testclient import TestClient -from src.api import lifecycle -from src.api.schema import ( +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import ( ImageSpec, ListSandboxesResponse, PaginationInfo, diff --git a/server/tests/test_routes_pause_resume.py b/server/tests/test_routes_pause_resume.py index 6551dfe07..db3359359 100644 --- a/server/tests/test_routes_pause_resume.py +++ b/server/tests/test_routes_pause_resume.py @@ -15,7 +15,7 @@ from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient -from src.api import lifecycle +from opensandbox_server.api import lifecycle def test_pause_route_calls_service_and_returns_202( diff --git a/server/tests/test_routes_proxy.py b/server/tests/test_routes_proxy.py index c89b25ee7..d7f9ef461 100644 --- a/server/tests/test_routes_proxy.py +++ b/server/tests/test_routes_proxy.py @@ -19,11 +19,11 @@ from fastapi.testclient import TestClient from websockets.typing import Origin -import src.api.proxy as proxy_api -from src.api import lifecycle -from src.api.schema import Endpoint -from src.middleware.auth import SANDBOX_API_KEY_HEADER -from src.services.constants import OPEN_SANDBOX_EGRESS_AUTH_HEADER, OPEN_SANDBOX_INGRESS_HEADER +import opensandbox_server.api.proxy as proxy_api +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import Endpoint +from opensandbox_server.middleware.auth import SANDBOX_API_KEY_HEADER +from opensandbox_server.services.constants import OPEN_SANDBOX_EGRESS_AUTH_HEADER, OPEN_SANDBOX_INGRESS_HEADER class _FakeStreamingResponse: diff --git a/server/tests/test_routes_renew_expiration.py b/server/tests/test_routes_renew_expiration.py index 9105d4c63..c8cdbc300 100644 --- a/server/tests/test_routes_renew_expiration.py +++ b/server/tests/test_routes_renew_expiration.py @@ -17,8 +17,8 @@ from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient -from src.api import lifecycle -from src.api.schema import RenewSandboxExpirationResponse +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import RenewSandboxExpirationResponse def test_renew_expiration_returns_updated_timestamp( diff --git a/server/tests/test_schema.py b/server/tests/test_schema.py index c459d91cc..01c9732ba 100644 --- a/server/tests/test_schema.py +++ b/server/tests/test_schema.py @@ -17,7 +17,7 @@ import pytest from pydantic import ValidationError -from src.api.schema import ( +from opensandbox_server.api.schema import ( CreateSandboxRequest, Host, ImageSpec, diff --git a/server/tests/test_validators.py b/server/tests/test_validators.py index 180dcb714..fe1ff7da7 100644 --- a/server/tests/test_validators.py +++ b/server/tests/test_validators.py @@ -15,9 +15,9 @@ import pytest from fastapi import HTTPException -from src.api.schema import Host, OSSFS, PVC, Volume -from src.services.constants import SandboxErrorCodes -from src.services.validators import ( +from opensandbox_server.api.schema import Host, OSSFS, PVC, Volume +from opensandbox_server.services.constants import SandboxErrorCodes +from opensandbox_server.services.validators import ( ensure_metadata_labels, ensure_timeout_within_limit, ensure_valid_host_path, diff --git a/tests/python/tests/test_sandbox_e2e.py b/tests/python/tests/test_sandbox_e2e.py index 3b01b44ae..ca94074f0 100644 --- a/tests/python/tests/test_sandbox_e2e.py +++ b/tests/python/tests/test_sandbox_e2e.py @@ -67,7 +67,7 @@ logger = logging.getLogger(__name__) -# Keep in sync with server ``src/extensions/keys.py`` +# Keep in sync with server ``opensandbox_server/extensions/keys.py`` ACCESS_RENEW_EXTEND_SECONDS_KEY = "access.renew.extend.seconds" diff --git a/tests/python/tests/test_sandbox_manager_e2e_sync.py b/tests/python/tests/test_sandbox_manager_e2e_sync.py index b5a5336c6..c8c438149 100644 --- a/tests/python/tests/test_sandbox_manager_e2e_sync.py +++ b/tests/python/tests/test_sandbox_manager_e2e_sync.py @@ -23,12 +23,11 @@ We create 3 dedicated sandboxes per run to keep assertions deterministic. """ +import logging import time from datetime import timedelta from uuid import uuid4 -import logging - import pytest from opensandbox import SandboxManagerSync, SandboxSync from opensandbox.exceptions import SandboxApiException