Skip to content

Commit dc68915

Browse files
committed
feat: add async sandbox commit
Closes #1279
1 parent ec91d65 commit dc68915

13 files changed

Lines changed: 984 additions & 25 deletions

File tree

rock/actions/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
CloseResponse,
2424
CloseSessionResponse,
2525
CommandResponse,
26+
CommitErrorCode,
27+
CommitPhase,
28+
CommitStatusResponse,
2629
CreateBashSessionResponse,
2730
CreateSessionResponse,
2831
ExecuteBashSessionResponse,
@@ -69,6 +72,9 @@
6972
"IsAliveResponse",
7073
"SandboxStatusResponse",
7174
"CommandResponse",
75+
"CommitPhase",
76+
"CommitErrorCode",
77+
"CommitStatusResponse",
7278
"WriteFileResponse",
7379
"OssSetupResponse",
7480
"ExecuteBashSessionResponse",

rock/actions/sandbox/response.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,40 @@ class SandboxStatusResponse(BaseModel):
6767
state_history: list[StateTransitionRecord] = []
6868

6969

70+
class CommitPhase(str, Enum):
71+
RUNNING = "RUNNING"
72+
SUCCEEDED = "SUCCEEDED"
73+
FAILED = "FAILED"
74+
75+
76+
class CommitErrorCode(str, Enum):
77+
SANDBOX_NOT_FOUND = "SANDBOX_NOT_FOUND"
78+
WORKER_NOT_FOUND = "WORKER_NOT_FOUND"
79+
WORKER_UNREACHABLE = "WORKER_UNREACHABLE"
80+
SANDBOX_CONTAINER_NOT_FOUND = "SANDBOX_CONTAINER_NOT_FOUND"
81+
COMMIT_CONFLICT = "COMMIT_CONFLICT"
82+
DISPATCH_FAILED = "DISPATCH_FAILED"
83+
LOGIN_FAILED = "LOGIN_FAILED"
84+
COMMIT_FAILED = "COMMIT_FAILED"
85+
PUSH_FAILED = "PUSH_FAILED"
86+
TIMEOUT = "TIMEOUT"
87+
PROCESS_LOST = "PROCESS_LOST"
88+
STATUS_NOT_FOUND = "STATUS_NOT_FOUND"
89+
STATUS_CORRUPTED = "STATUS_CORRUPTED"
90+
91+
92+
class CommitStatusResponse(BaseModel):
93+
sandbox_id: str
94+
image_tag: str
95+
phase: CommitPhase
96+
started_at: str
97+
completed_at: str | None = None
98+
exit_code: int | None = None
99+
failed_stage: str | None = None
100+
error_code: CommitErrorCode | None = None
101+
error_message: str | None = None
102+
103+
70104
class CommandResponse(BaseModel):
71105
stdout: str = ""
72106
stderr: str = ""

rock/admin/entrypoints/sandbox_proxy_api.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
BashObservation,
99
CloseBashSessionResponse,
1010
CommandResponse,
11+
CommitStatusResponse,
1112
CreateBashSessionResponse,
1213
ReadFileResponse,
1314
ResponseStatus,
1415
RockResponse,
16+
SandboxResponse,
1517
UploadResponse,
1618
WriteFileResponse,
1719
)
1820
from rock.admin.proto.request import (
1921
BatchSandboxStatusRequest,
22+
CommitRequest,
2023
SandboxBashAction,
2124
SandboxCloseBashSessionRequest,
2225
SandboxCommand,
@@ -28,7 +31,7 @@
2831
from rock.admin.proto.response import BatchSandboxStatusResponse, SandboxListResponse, SandboxStatusResponse
2932
from rock.common.exception import handle_exceptions
3033
from rock.common.port_validation import validate_port_forward_port
31-
from rock.common.validation import NonBlankStr
34+
from rock.common.validation import NonBlankStr, SandboxContainerName
3235
from rock.logger import init_logger
3336
from rock.sandbox.service.sandbox_proxy_service import SandboxProxyService
3437
from rock.sdk.common.exceptions import BadRequestRockError
@@ -115,6 +118,18 @@ async def execute(command: SandboxCommand) -> RockResponse[CommandResponse]:
115118
return RockResponse(result=await sandbox_proxy_service.execute(command))
116119

117120

121+
@sandbox_proxy_router.post("/commit")
122+
@handle_exceptions(error_message="commit sandbox failed")
123+
async def commit(request: CommitRequest) -> RockResponse[CommitStatusResponse | SandboxResponse]:
124+
return RockResponse(result=await sandbox_proxy_service.commit(request))
125+
126+
127+
@sandbox_proxy_router.get("/commit/{sandbox_id}")
128+
@handle_exceptions(error_message="get commit status failed")
129+
async def get_commit_status(sandbox_id: SandboxContainerName) -> RockResponse[CommitStatusResponse | SandboxResponse]:
130+
return RockResponse(result=await sandbox_proxy_service.get_commit_status(sandbox_id))
131+
132+
118133
@sandbox_proxy_router.post("/create_session")
119134
@handle_exceptions(error_message="create session failed")
120135
async def create_session(request: SandboxCreateBashSessionRequest) -> RockResponse[CreateBashSessionResponse]:

rock/admin/proto/request.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Annotated, Literal, TypedDict
22

33
from fastapi import Header
4-
from pydantic import BaseModel, Field
4+
from pydantic import BaseModel, Field, field_validator
55

66
from rock import env_vars
77
from rock.actions import (
@@ -12,7 +12,7 @@
1212
ReadFileRequest,
1313
WriteFileRequest,
1414
)
15-
from rock.common.validation import NonBlankStr
15+
from rock.common.validation import NonBlankStr, SandboxContainerName
1616

1717

1818
class SandboxStartRequest(BaseModel):
@@ -50,6 +50,23 @@ class SandboxStartRequest(BaseModel):
5050
"""GPU accelerator type (e.g. 'A100', 'V100'). If not specified, any available GPU will be used."""
5151

5252

53+
class CommitRequest(BaseModel):
54+
sandbox_id: SandboxContainerName
55+
image_tag: NonBlankStr
56+
username: str
57+
password: str
58+
59+
@field_validator("image_tag", mode="before")
60+
@classmethod
61+
def validate_image_tag_for_status_file(cls, value: object) -> object:
62+
if isinstance(value, str) and any(
63+
character in value
64+
for character in ("\x00", "\n", "\r", "\v", "\f", "\x1c", "\x1d", "\x1e", "\x85", "\u2028", "\u2029")
65+
):
66+
raise ValueError("image_tag cannot contain NUL or line separators")
67+
return value
68+
69+
5370
class SandboxCommand(Command):
5471
timeout: float | None = 1200
5572
"""The timeout for the command. None means no timeout."""

rock/common/validation.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,16 @@
1616
handler in ``rock.common.exception.request_validation_exception_handler`` maps
1717
back to the project's ``RockResponse(status=Failed, error=...)`` envelope.
1818
"""
19+
20+
21+
SANDBOX_CONTAINER_NAME_PATTERN = r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$"
22+
23+
SandboxContainerName = Annotated[
24+
str,
25+
StringConstraints(
26+
strip_whitespace=True,
27+
min_length=1,
28+
max_length=128,
29+
pattern=SANDBOX_CONTAINER_NAME_PATTERN,
30+
),
31+
]

0 commit comments

Comments
 (0)