fix(CubeAPI): propagate create-time env vars to envd-backed commands#566
Conversation
273018b to
2779e07
Compare
| return Ok(()); | ||
| }; | ||
| let url = build_envd_init_url(base_url, sandbox_id); | ||
| let resp = self |
There was a problem hiding this comment.
Missing HTTP timeout on envd init call – The shared reqwest::Client in state.rs (line 46-50) is built without .timeout(), which means reqwest::Client defaults to no timeout. If the envd proxy is unresponsive, this POST will hang indefinitely, blocking the sandbox creation endpoint for that handler. Since this call is best-effort (failure is logged and swallowed), it should have a tight timeout so it fails fast.
| let resp = self | |
| let resp = self | |
| .http_client | |
| .post(url) | |
| .json(&EnvdInitRequest { env_vars }) | |
| .timeout(std::time::Duration::from_secs(10)) | |
| .send() | |
| .await | |
| .map_err(|e| AppError::Internal(anyhow::anyhow!("envd init request failed: {}", e)))?; |
| let url = build_envd_init_url(base_url, sandbox_id); | ||
| let resp = self | ||
| .http_client | ||
| .post(url) |
There was a problem hiding this comment.
Missing auth header on envd init request – Every other call to the sandbox proxy (e.g. run_envd_command in agenthub.rs:2162) sends Authorization: Basic cm9vdDo=. This new init_sandbox_env_vars method sends no auth header. If the downstream /init endpoint enforces the same proxy-level auth, this will silently fail on every request (logged at warn, caller sees success). If it does not enforce auth, this is an inconsistency that may widen the attack surface.
| .post(url) | |
| .http_client | |
| .post(url) | |
| .header("Authorization", "Basic cm9vdDo=") | |
| .json(&EnvdInitRequest { env_vars }) |
|
|
||
| async fn init_sandbox_env_vars( | ||
| &self, | ||
| sandbox_id: &str, |
There was a problem hiding this comment.
Silent skip when env vars are provided but no proxy URL is configured – When sandbox_proxy_base_url is None and env_vars is non-empty, the init call is skipped silently. The caller (create_sandbox) only logs a warning when the HTTP call itself fails, not when it's skipped. This makes it hard for operators to debug why client-provided env vars are being silently ignored. Consider adding a tracing::warn! here when env_vars is non-empty but the proxy URL is unset.
| env_vars: &'a HashMap<String, String>, | ||
| } | ||
|
|
||
| pub(crate) fn default_sandbox_proxy_base_url() -> Option<String> { |
There was a problem hiding this comment.
pub(crate) function should have a doc comment – default_sandbox_proxy_base_url is visible outside this module (called from services/mod.rs:104) but has no doc comment. It reads three environment variables with a fallback chain. A doc comment explaining the env vars (AGENTHUB_SANDBOX_PROXY_URL, CUBE_SANDBOX_NODE_IP, CUBE_PROXY_HTTP_PORT), their meanings, and the fallback priority would help future maintainers.
| Path((sandbox_id, port)): Path<(String, u16)>, | ||
| Json(body): Json<Value>, | ||
| ) -> StatusCode { | ||
| assert_eq!(sandbox_id, "sb-123"); |
There was a problem hiding this comment.
Assertions inside spawned test handler are invisible on failure – These assert_eq! calls run inside a tokio::spawn-ed axum handler. If they fail (say from a code change that alters the URL path), the panic is caught by tokio and logged to stderr, but does not cause the test to fail. The test would silently pass because the handler returning an error response triggers the best-effort fallback. Consider returning the received values in the response and asserting from the caller side, or using a tokio::sync::oneshot channel to propagate assertion failures.
|
Are you doing the same thing as PR #554? |
Thanks for pointing this out. It is related to #554, but the fix is applied at a different layer. #554 calls envd The main reason is compatibility with clients that do not use the CubeSandbox SDK, especially E2B SDK users, since the E2B SDK sends |
34e765f to
c466139
Compare
Review: PR #566 — Propagate create-time env vars to envd-backed commandsOverall: This is a well-structured PR with clear data flow (CubeAPI -> CubeMaster -> Cubelet -> envd), thorough input validation, and backward compatibility. The design is sound and the happy-path test coverage is solid. Below are findings that merit attention before merge. 1. Zombie sandbox on envd init failure (High)Cubelet/services/cubebox/cube_container_create.go:339 When doCreateTimeEnvdInit fails, the function returns an error but containers have already been created, probed, and post-create hooks have run (lines 331-337). There is no cleanup. The sandbox becomes a zombie consuming resources. Fix: tear down containers on envd init failure, or at minimum mark the sandbox as failed for garbage collection. 2. Synchronous envd init blocks create path (Medium)Cubelet/services/cubebox/cube_container_create.go:339 The envd init HTTP call blocks sandbox creation for up to 10s. This directly inflates p95/p99 create latency. Consider a shorter dial timeout or asynchronous init. 3. No aggregate env var payload size limit (Medium)CubeMaster/pkg/service/sandbox/util.go:692 Individual values are capped at 4KB, but there is no limit on total payload size. Hundreds of large env vars could exceed the approx 256KB Kubernetes annotation limit. Add a cap after marshaling. 4. Documentation: Missing files in directory listing (Medium)examples/code-sandbox-quickstart/README.md and README_zh.md The directory tree is missing create_with_envs.py (the new file) and env_utils.py (shared loader). Both should be added to the listing. 5. Documentation: envs vs env_vars parameter name (Low)examples/code-sandbox-quickstart/create_with_envs.py:13 The example uses envs=... but the SDK Sandbox.create() uses env_vars=. It works via **kwargs but wont appear in IDE autocompletion. Consider using env_vars= for consistency. Summary
Inline comments cover the HTTP client redirect behavior, io.ReadAll error discard, mutable global port variable, and test race conditions in more detail. Overall the approach is sound and core functionality works correctly. The main concerns are operational reliability (zombie cleanup, aggregate size limits). |
851ec0d to
c7f0071
Compare
Yes, we can probe envd's /health at the stage of creating templates to ensure that envd has started and is ready |
|
@kinwin-ustc Added a short bounded retry for envd init. The current behavior is:
This keeps fail-fast semantics while tolerating a small restore/readiness jitter window. |
Sounds good. Later I can work on a follow-up to probe envd |
|
Another issue worth discussing is forward compatibility. The |
I agree this changes behavior for legacy templates. I kept the explicit rejection mainly to align with the direction of using the #650 envd annotation as the capability signal. My understanding is that if create-time env vars are requested, a missing If we silently fall back to probing the default envd endpoint when the annotation is missing, the capability check becomes more like runtime guessing again, which weakens the #650 signal for this path. So the current behavior is intentionally stricter: ordinary sandbox creation without env vars remains backward compatible, but create-time env injection requires a template carrying the envd capability annotation. I agree this should be clearly documented as a compatibility / migration note. If we want legacy-template fallback, I think that should be a separate policy decision or follow-up. |
|
I believe backward compatibility is very important. Imagine that someone has already built thousands of templates, and redoing the templates would be a huge amount of work. Perhaps we can judge the availability of envd through the probe itself. If envd is not installed, then 49983 is likely not listening, and it will immediately return reset. If 49983 is being listened to by non-envd, it will not respond correctly to our requests and will immediately fail in very quick retries. This can ensure that users who have already deployed it can enjoy the new features very lightly, and it will not have any impact on the semantics of the function itself |
|
@kinwin-ustc Updated for backward compatibility. If Also added focused tests for the missing-annotation fallback path. |
|
LGTM |
|
@liciazhu Could you please help test this pr? |
OK |
|
TestCleanupAfterEnvdInitFailurePassesExpectedContextAndRequest constructs CubeBox with Metadata.Namespace = "ns-cleanup", but cleanupAfterEnvdInitFailure reads sandBox.Namespace (the top-level field on CubeBox), which shadows Metadata.Namespace due to Go's field promotion rules. As a result, the top-level Namespace is empty, namespaces.WithNamespace is never called, and namespaces.NamespaceRequired(ctx) fails. Fix: set CubeBox.Namespace directly instead of CubeBox.Metadata.Namespace. |
572622a to
af21bc4
Compare
Fixed, I reran the focused tests, including the new |
Create-time env vars were dropped during sandbox creation, so envd-backed command execution (e.g. commands.run) could not see them. Forward env vars from CubeAPI into CubeMaster, serialize them into an internal annotation, and have cubelet initialize envd via its /init data-plane endpoint after sandbox startup. Gate on the propagated envd capability signal (cube.master.components.envd.version) and fail fast when injection is requested on a template without it. Scope is limited to the envd data-plane init path; container-level startup env refresh is intentionally not included. Add focused tests across CubeAPI/CubeMaster/cubelet and a quickstart example. Signed-off-by: zhengyilei <zheng_yilei@qq.com> Co-authored-by: jinlong <jinlong@tencent.com>
af21bc4 to
bb751cb
Compare
| }) | ||
| } | ||
|
|
||
| func (l *local) destroySandbox(ctx context.Context, opts *workflow.DestroyContext) error { |
There was a problem hiding this comment.
This function name can be more strictly limited to cleaning up after envd init fails; the current name is easily mistaken for the main entrance to destroy
There was a problem hiding this comment.
This function name can be more strictly limited to cleaning up after envd init fails; the current name is easily mistaken for the main entrance to destroy
Good point. I narrowed the helper name to destroySandboxAfterEnvdInitFailure.
bb751cb to
b8e5058
Compare
Keep create-time env var injection backward-compatible for templates built before envd capability propagation. When create_time_env_vars are requested and the template lacks the envd support annotation, probe the default envd init endpoint with bounded retry instead of rejecting upfront. If init still fails, return an explicit error instead of silently dropping the env vars. Also surface missing-annotation context in failure messages and add focused tests for the fallback path. Signed-off-by: zhengyilei <zheng_yilei@qq.com>
b8e5058 to
ff45619
Compare
|
@zyl1121 I will merge this PR. Thanks for your contribution! If you're interested, feel free to open another PR to update the env-related documentation |
Motivation
For envd-backed command execution, create-time environment variables should be propagated through the sandbox create path and initialized inside the sandbox runtime.
On the current CubeAPI sandbox creation path, this propagation is incomplete:
envs, while CubeAPI only acceptedenvVarsAs a result, envd-backed execution such as
commands.runcannot see environment variables passed at sandbox creation time.This PR does not introduce a generic sandbox startup-time environment refresh model across all runtime paths.
What this PR changes
This PR fixes the create-time env propagation chain for envd-backed command execution:
envsas an alias ofenvVarsinNewSandbox, so both E2B SDK and CubeSandbox SDK callers are supportedcreate_time_env_vars/initafter sandbox startup and probe successcube.master.components.envd.version, when present/init, bounded retry, and the missing-annotation fallback pathOn the main CubeMaster-managed create path, failures after sandbox allocation are still covered by CubeMaster's async destroy compensation. The additional cubelet-side cleanup keeps the runtime-local fail-fast path self-contained and also protects direct cubelet callers.
Scope
This PR restores create-time environment variables for envd-backed execution, such as
commands.run.It does not change
run_code.run_codegoes through the bundled lightweight code interpreter instead of envd-backed process execution, and that interpreter does not currently read envd/envs.It also does not claim generic startup-time env refresh support for every sandbox runtime path.
Testing
Unit tests:
Cluster validation:
Before this PR, on current
origin/master:After this PR:
Verified behavior:
envspathenvVarspathcommands.runsees create-time env vars after sandbox creationcommands.runcommands.runfalls back to the sandbox-level env varsrun_coderemains<NOT SET>on the current runtime imageCompatibility
This change is backward compatible for existing create requests:
envVarsrequests continue to workenvsrequests now workMigration / Compatibility Note
When present, cubelet uses the propagated envd support annotation:
Templates created before envd capability reporting was introduced may not have this annotation yet. For those templates:
BOOT -> SNAPSHOTflow will carry explicit envd support metadata and avoid relying on the compatibility fallbackFor envd-backed command execution, precedence remains:
Documentation
This PR includes a small quickstart visibility update in
examples/code-sandbox-quickstart:README.mdREADME_zh.mdcreate_with_envs.pyThe behavior change is intentionally narrow: the sandbox create path now propagates create-time environment variables through CubeAPI, CubeMaster, and cubelet, and treats failed envd initialization as a create failure for envd-backed command execution.
The remaining
run_codebehavior is tracked in the linked issue and requires runtime/image-side support in the bundled lightweight code interpreter.Related issue
Refs #565