Skip to content

Commit 46dbec1

Browse files
committed
feat(admin): add POST /sandboxes/{sandbox_id}/archive endpoint
RESTful-style archive endpoint following the same pattern as POST /sandboxes/{sandbox_id}/restart (0632c8b).
1 parent 053599e commit 46dbec1

3 files changed

Lines changed: 56 additions & 3 deletions

File tree

rock/admin/entrypoints/sandbox_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,10 @@ async def delete(sandbox_id: str = Body(..., embed=True)) -> RockResponse:
452452
return RockResponse(result=f"{sandbox_id} deleted")
453453

454454

455-
@sandbox_router.post("/archive")
455+
@sandbox_router.post("/sandboxes/{sandbox_id}/archive")
456456
@handle_exceptions(error_message="archive sandbox failed")
457457
async def archive(
458-
sandbox_id: str = Body(..., embed=True),
458+
sandbox_id: NonBlankStr,
459459
rock_authorization: str | None = Header(default=None, alias="X-Key"),
460460
) -> RockResponse:
461461
archive_cfg = sandbox_manager.rock_config.lifecycle.archive

rock/deployments/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ class DockerDeploymentConfig(DeploymentConfig):
118118
"""Custom name for the container. If None, a random name will be generated."""
119119

120120
auto_delete_seconds: int | None = None
121-
"""If set, the container will be automatically deleted after container stopped."""
121+
"""Per-sandbox auto-delete policy. 0 = --rm (immediate delete on stop);
122+
>0 = delete after this many seconds idle; None = fall back to global
123+
lifecycle.auto_delete_after_sec."""
122124

123125
type: Literal["docker"] = "docker"
124126
"""Deployment type discriminator for serialization/deserialization and CLI parsing. Should not be modified."""
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests for RESTful sandbox endpoints on sandbox_router."""
2+
3+
from unittest.mock import AsyncMock, MagicMock
4+
5+
import pytest
6+
from fastapi import FastAPI
7+
from fastapi.exceptions import RequestValidationError
8+
from httpx import ASGITransport, AsyncClient
9+
10+
from rock.admin.entrypoints.sandbox_api import sandbox_router, set_sandbox_manager
11+
from rock.common.exception import request_validation_exception_handler
12+
13+
14+
@pytest.fixture
15+
def sandbox_app():
16+
mock_manager = MagicMock()
17+
mock_manager.rock_config = MagicMock()
18+
mock_manager.rock_config.nacos_provider = None
19+
set_sandbox_manager(mock_manager)
20+
app = FastAPI()
21+
app.add_exception_handler(RequestValidationError, request_validation_exception_handler)
22+
app.include_router(sandbox_router, prefix="/apis/envs/sandbox/v1")
23+
return app, mock_manager
24+
25+
26+
BASE = "/apis/envs/sandbox/v1"
27+
28+
29+
@pytest.mark.asyncio
30+
async def test_archive_allowed(sandbox_app):
31+
app, mock_manager = sandbox_app
32+
mock_manager.rock_config.lifecycle.archive.is_allowed.return_value = True
33+
mock_manager.archive_sandbox = AsyncMock()
34+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
35+
resp = await client.post(f"{BASE}/sandboxes/sb-1/archive")
36+
assert resp.status_code == 200
37+
assert resp.json()["status"] == "Success"
38+
mock_manager.archive_sandbox.assert_called_once_with("sb-1")
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_archive_forbidden(sandbox_app):
43+
"""Unauthorised key: handle_exceptions returns HTTP 200 with status=Failed."""
44+
app, mock_manager = sandbox_app
45+
mock_manager.rock_config.lifecycle.archive.is_allowed.return_value = False
46+
mock_manager.archive_sandbox = AsyncMock()
47+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
48+
resp = await client.post(f"{BASE}/sandboxes/sb-1/archive", headers={"X-Key": "bad-key"})
49+
assert resp.status_code == 200
50+
assert resp.json()["status"] == "Failed"
51+
mock_manager.archive_sandbox.assert_not_called()

0 commit comments

Comments
 (0)