|
3 | 3 |
|
4 | 4 | from __future__ import annotations |
5 | 5 |
|
| 6 | +import random |
| 7 | +import time |
| 8 | +from datetime import datetime, timezone |
6 | 9 | from typing import Any, Callable, Dict |
7 | 10 |
|
8 | 11 | import httpx |
9 | 12 | import requests |
10 | 13 |
|
11 | | -from ._commands import CommandResult, Commands |
| 14 | +from ._commands import CommandResult, Commands, ENVD_PORT |
12 | 15 | from ._config import Config |
13 | 16 | from ._exceptions import ApiError, AuthenticationError, CubeSandboxError, SandboxNotFoundError, TemplateNotFoundError |
14 | 17 | from ._filesystem import Filesystem |
|
18 | 21 | from ._transport import build_client |
19 | 22 |
|
20 | 23 | JUPYTER_PORT = 49999 |
| 24 | +ENVD_INIT_MAX_ATTEMPTS = 5 |
| 25 | +ENVD_INIT_RETRY_BASE_SECS = 0.8 |
| 26 | +ENVD_INIT_RETRY_JITTER_SECS = 0.4 |
| 27 | +ENVD_INIT_REQ_TIMEOUT_SECS = 10.0 |
21 | 28 |
|
22 | 29 |
|
23 | 30 | def _check_response(resp: requests.Response) -> None: |
@@ -156,7 +163,10 @@ def create( |
156 | 163 | resp = s.post(f"{cfg.api_url}/sandboxes", json=payload, |
157 | 164 | headers={"Content-Type": "application/json"}) |
158 | 165 | _check_response(resp) |
159 | | - return cls(resp.json(), config=cfg) |
| 166 | + sandbox = cls(resp.json(), config=cfg) |
| 167 | + if env_vars: |
| 168 | + sandbox._init_env_vars(env_vars) |
| 169 | + return sandbox |
160 | 170 |
|
161 | 171 | @classmethod |
162 | 172 | def connect(cls, sandbox_id: str, *, config: Config | None = None) -> "Sandbox": |
@@ -667,3 +677,64 @@ def _build_session(self) -> requests.Session: |
667 | 677 | def _build_data_client(self) -> httpx.Client: |
668 | 678 | """Build an HTTP client for CubeProxy-routed sandbox data-plane APIs.""" |
669 | 679 | return build_client(self._config) |
| 680 | + |
| 681 | + def _init_env_vars(self, env_vars: Dict[str, str]) -> None: |
| 682 | + """Make create-time env_vars visible to later commands.run / run_code. |
| 683 | +
|
| 684 | + The control plane only records env_vars as sandbox metadata; it never |
| 685 | + loads them into the guest runtime. So once the sandbox is up we push |
| 686 | + them into the guest via envd's native POST /init, reusing the exact |
| 687 | + CubeProxy data-plane channel commands.run already uses. envd stores them |
| 688 | + as global defaults and merges them into every later process execution, |
| 689 | + giving the precedence ``template env < create env < per-command env``. |
| 690 | +
|
| 691 | + Routing through the configured data-plane client means no extra |
| 692 | + deployment configuration is required: whatever address commands.run |
| 693 | + reaches envd on, /init reaches it on too. |
| 694 | + """ |
| 695 | + if not env_vars: |
| 696 | + return |
| 697 | + if self._client is None: |
| 698 | + self._client = self._build_data_client() |
| 699 | + |
| 700 | + headers = {} |
| 701 | + access_token = self._data.get("envdAccessToken") |
| 702 | + if access_token: |
| 703 | + headers["X-Access-Token"] = access_token |
| 704 | + |
| 705 | + url = f"http://{self.get_host(ENVD_PORT)}/init" |
| 706 | + body = { |
| 707 | + "envVars": env_vars, |
| 708 | + "timestamp": datetime.now(timezone.utc).isoformat(), |
| 709 | + } |
| 710 | + |
| 711 | + # The proxy route to a freshly created sandbox may settle a moment after |
| 712 | + # create returns, so retry briefly before surfacing a hard failure. |
| 713 | + last_error: Exception | None = None |
| 714 | + for attempt in range(ENVD_INIT_MAX_ATTEMPTS): |
| 715 | + if attempt: |
| 716 | + delay = ENVD_INIT_RETRY_BASE_SECS + random.uniform( |
| 717 | + 0, ENVD_INIT_RETRY_JITTER_SECS |
| 718 | + ) |
| 719 | + time.sleep(delay) |
| 720 | + try: |
| 721 | + resp = self._client.post( |
| 722 | + url, |
| 723 | + json=body, |
| 724 | + headers=headers, |
| 725 | + timeout=ENVD_INIT_REQ_TIMEOUT_SECS, |
| 726 | + ) |
| 727 | + try: |
| 728 | + if resp.status_code < 400: |
| 729 | + return |
| 730 | + last_error = RuntimeError( |
| 731 | + f"envd /init returned HTTP {resp.status_code}" |
| 732 | + ) |
| 733 | + finally: |
| 734 | + resp.close() |
| 735 | + except BaseException as exc: |
| 736 | + if isinstance(exc, (KeyboardInterrupt, SystemExit)): |
| 737 | + raise |
| 738 | + last_error = exc |
| 739 | + |
| 740 | + raise CubeSandboxError(f"failed to inject create env_vars into sandbox: {last_error}") |
0 commit comments