Skip to content

Add Gin API sandbox example#683

Open
hyy321 wants to merge 1 commit into
TencentCloud:masterfrom
hyy321:add-gin-api-sandbox-example
Open

Add Gin API sandbox example#683
hyy321 wants to merge 1 commit into
TencentCloud:masterfrom
hyy321:add-gin-api-sandbox-example

Conversation

@hyy321

@hyy321 hyy321 commented Jun 30, 2026

Copy link
Copy Markdown

This PR adds a Gin API sandbox example.

It includes:

  • Dockerfile for building a Go/Gin runtime template
  • E2B-compatible SDK example for creating a sandbox from the template
  • Multi-file Gin project written into /workspace/app
  • Build and start flow with go mod tidy and go build
  • HTTP endpoint tests for /health, /users/:id, and POST /users
  • README with use cases, resource recommendations, known limitations, and deployment notes

Verified locally with CubeSandbox deployment.

Related to #682

@hyy321 hyy321 requested a review from tinklone as a code owner June 30, 2026 12:41
@hyy321 hyy321 force-pushed the add-gin-api-sandbox-example branch from 43d3721 to 83b9e77 Compare June 30, 2026 13:07
Comment thread examples/gin-api-sandbox/Dockerfile Outdated
# Runtime flow:
# 1. cubesandbox-base entrypoint starts envd on :49983.
# 2. SDK writes Agent-generated go.mod / main.go / test files into /workspace/app.
# 3. SDK runs go mod tidy / go test / go build.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation mismatch: The comment says the SDK runs go test, but official_sdk_gin_run.py only runs go mod tidy and go build. Either fix the comment or add go test to the script (which would also require writing _test.go files).

Comment thread examples/gin-api-sandbox/Dockerfile Outdated
# Workspace used by SDK.
# Agent-generated files will be written into /workspace/app.
RUN mkdir -p /workspace/app /workspace/go /workspace/.cache/go-build \
&& chmod -R 777 /workspace

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly permissive: chmod -R 777 on the entire /workspace tree grants world-writable access to every file. Any process in the container — including the compiled Go binary or a compromised dependency — can read, modify, or delete the Go module cache, build artifacts, and source files. Consider using 755 and restricting write access to only the paths that need it.

Comment thread examples/gin-api-sandbox/Dockerfile Outdated

# Persist Go module proxy config inside the image.
# This does not install project dependencies; it only changes where Go downloads modules from.
RUN go env -w GOPROXY=https://goproxy.cn,direct \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider consolidating layers. This RUN, the symlink/profile setup (line 54), and the mkdir/chmod (line 66) could all be merged into the Go install layer (line 48) to reduce image layers and one redundant go binary invocation.


load_dotenv(".env", override=True)

template_id = os.environ["CUBE_TEMPLATE_ID"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing env var validation: A bare KeyError is raised if CUBE_TEMPLATE_ID is unset. Since load_dotenv(.env) silently does nothing if .env doesn't exist, the error message is unhelpful. Consider using os.getenv() with a guard that explains how to set up the environment, e.g. Copy .env.example to .env and fill in the values.

from dotenv import load_dotenv
from e2b_code_interpreter import Sandbox

load_dotenv(".env", override=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

override=True silently overwrites existing env vars. If E2B_API_KEY is already set in the system environment (e.g., by CI/CD), the .env file value replaces it without warning. Consider using override=False (the default) so system-set variables take precedence.

"pgrep -f '^/tmp/gin-app$' | xargs -r kill; "
"rm -f /tmp/gin.log; "
"nohup /tmp/gin-app > /tmp/gin.log 2>&1 & "
"sleep 2; "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The fixed sleep 2 assumes the server will be ready within 2 seconds, which may not hold in a cold or loaded sandbox. echo started always prints regardless of whether the process actually started. Consider replacing with a polling loop: for i in $(seq 1 10); do if curl -sf localhost:8080/health >/dev/null 2>&1; then echo ready; break; fi; sleep 0.5; done

@@ -0,0 +1,3 @@
e2b
python-dotenv
requests No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused dependency: requests is listed but never imported or used in official_sdk_gin_run.py (the script uses curl inside the sandbox for HTTP tests). Remove it unless there's a planned use. Also consider pinning versions for reproducibility.

--image gin-api-sandbox:latest \
--writable-layer-size 5Gi \
--expose-port 49983 \
--expose-port 49999 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Port 49999 is unexplained. This is exposed and set as the health probe, but the example never starts a listener on it. The Deployment Notes say "Port 49999 is used for the template health probe" but don't explain who handles it. A reader may wonder why no service is bound to that port. Consider adding a note that CubeSandbox infrastructure handles the health probe on this port automatically.

@hyy321 hyy321 force-pushed the add-gin-api-sandbox-example branch from 83b9e77 to 61f3963 Compare June 30, 2026 14:38
}
'''

with Sandbox.create(template=template_id) as sandbox:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Wrap the execution block in if __name__ == "__main__":.

Without this guard, importing any function from this file would trigger sandbox creation as a side effect. For an example that others may reference, following standard Python practice helps prevent accidental execution.

def main():
    with Sandbox.create(template=template_id) as sandbox:
        ...

if __name__ == "__main__":
    main()
print(result.stderr)

print("\n== write agent generated files ==")
sandbox.files.write("/workspace/app/go.mod", go_mod)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Check the return values of sandbox.files.write() calls.

If a write fails (disk full, permission denied, invalid path), the script won't detect it and will proceed to go mod tidy which will then fail with a confusing error. At minimum log the result, and ideally stop on failure.

print(result.stdout)
print(result.stderr)

print("\n== test GET /health ==")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Consider using curl -sSf (adding -f/--fail) to make these tests fail on HTTP errors.

Currently -sS returns exit code 0 for any valid HTTP response including 404 or 500, so these "tests" only verify the server is reachable — not that the endpoints return correct results. The server-startup polling above (line 187) correctly uses -f, but the actual endpoint tests don't.

Also consider checking that the response body contains expected content (e.g., "status":"ok"), or adding -f and checking result.exit_code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I updated the endpoint checks to use curl -fsS and added response body assertions so the script fails on HTTP errors or unexpected responses.

from dotenv import load_dotenv
from e2b_code_interpreter import Sandbox

load_dotenv(".env", override=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Use __file__-based path for the .env file.

load_dotenv(os.path.join(os.path.dirname(__file__), ".env"), override=False)

Relative ".env" silently fails to load if the script is invoked from a different working directory. The subsequent require_env call will then raise a RuntimeError but with a misleading message that points to copying .env.example, while the actual issue is that .env is in a different directory.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import os
from dotenv import load_dotenv
from e2b_code_interpreter import Sandbox

script_dir = os.path.dirname(os.path.abspath(file))
load_dotenv(os.path.join(script_dir, ".env"), override=False)

def require_env(name: str) -> str:
value = os.getenv(name)
if not value:
raise RuntimeError(
f"Missing required environment variable: {name}. "
"Copy examples/gin-api-sandbox/.env.example to examples/gin-api-sandbox/.env and fill in the values, "
"or export it in your shell environment."
)
return value

&& chmod 755 /workspace \
&& chmod -R 755 /workspace/app /workspace/go /workspace/.cache/go-build \
&& go env -w GOPROXY=https://goproxy.cn,direct GOSUMDB=sum.golang.google.cn
WORKDIR /workspace/app

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a USER directive for defense-in-depth.

The container runs as root end-to-end. If the Gin application were compromised (e.g., via a vulnerable dependency), the attacker gains root within the sandbox. Consider creating a non-root user:

RUN useradd -m -s /bin/bash ginsvc && chown -R ginsvc:ginsvc /workspace
USER ginsvc

This follows the principle of least privilege even in a sandboxed environment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. I updated the image to run as the existing non-root user from the CubeSandbox base image instead of root. I also adjusted ownership for /workspace and the required log files used by the inherited entrypoint. I verified that the template health probe starts correctly and that the SDK example can write files, run go mod tidy, build the Gin app, start the server, and pass all endpoint checks.

Comment thread examples/gin-api-sandbox/Dockerfile Outdated

# envd uses 49983.
# Agent-generated Gin services will usually listen on 8080.
EXPOSE 49983 8080

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add EXPOSE 49999 with a comment explaining its role, or add a comment here noting the discrepancy.

The README's cubemastercli template command (--expose-port 49999 --probe 49999) requires port 49999 for the health probe, but the Dockerfile only lists 49983 and 8080. Someone reading only the Dockerfile won't know this port is needed for template registration.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I clarified that port 49999 is reserved for the CubeSandbox template health probe and is handled by the CubeSandbox infrastructure. I also exposed 49999 in the Dockerfile with a matching comment.


```bash
cd /workspace/app && go mod tidy
cd /workspace/app && go build -o /tmp/gin-app .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The README shows only nohup /tmp/gin-app > /tmp/gin.log 2>&1 & but the actual script (lines 179-202) uses a richer command that also kills prior processes, cleans old logs, and polls /health for up to 10 seconds before proceeding. Consider either showing the actual command or adding a note explaining that the script includes readiness-wait logic.

@hyy321 hyy321 force-pushed the add-gin-api-sandbox-example branch from 61f3963 to 298cea0 Compare July 1, 2026 03:03
Comment thread examples/gin-api-sandbox/Dockerfile Outdated

# Install Go, configure shell environment, and prepare SDK workspace.
# This does not install project dependencies; it only configures the Go runtime.
RUN wget -O /tmp/go.tgz https://golang.google.cn/dl/go${GO_VERSION}.linux-amd64.tar.gz \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: Go tarball downloaded without checksum verification

The Go tarball is downloaded via wget but there is no sha256sum -c verification afterwards. If the download is corrupted (network issue) or the mirror is compromised, the image will silently accept a bad/malicious Go binary.

Go publishes checksums alongside each tarball. Consider adding verification like:

RUN wget -O /tmp/go.tgz ... \
    && wget -O /tmp/go.tgz.asc ... \
    && echo "<expected-sha256> /tmp/go.tgz" | sha256sum -c - \
    && rm -rf /usr/local/go \
    && tar -C /usr/local -xzf /tmp/go.tgz

Or, when using a stable Go version, pin the expected checksum as a build arg.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I added a pinned SHA256 checksum for the Go tarball and verify it with sha256sum -c - before extracting it.

Comment on lines +20 to +38
def write_file_checked(sandbox: Sandbox, path: str, content: str) -> None:
try:
result = sandbox.files.write(path, content)
except Exception as exc:
raise RuntimeError(f"Failed to write file {path}: {exc}") from exc

if result is not None:
print(f"write result for {path}: {result}")

verify = sandbox.commands.run(
f"test -f {shlex.quote(path)}",
timeout=30,
)
if verify.exit_code != 0:
raise RuntimeError(
f"File write verification failed for {path}\n"
f"stdout: {verify.stdout}\n"
f"stderr: {verify.stderr}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: write_file_checked doubles sandbox round-trips

Each call to write_file_checked writes a file and then immediately runs a separate test -f command in the sandbox to verify it exists. This doubles the number of API calls — 6 file writes become 12 sandbox operations, adding unnecessary latency (~500ms+ each).

sandbox.files.write() already returns success/failure (or raises on error). Remove lines 29-38 or gate the verification behind a debug flag. If content integrity is a concern, consider using a content hash instead of test -f.

Comment on lines +40 to +58
def run_http_check(sandbox: Sandbox, name: str, command: str, expected: str) -> None:
result = sandbox.commands.run(command, timeout=30)
print(f"\n== test {name} ==")
print("exit_code:", result.exit_code)
print(result.stdout)
print(result.stderr)

if result.exit_code != 0:
raise RuntimeError(
f"HTTP check failed for {name}\n"
f"stdout: {result.stdout}\n"
f"stderr: {result.stderr}"
)

if expected not in result.stdout:
raise RuntimeError(
f"Unexpected response for {name}. Expected to find {expected!r}\n"
f"stdout: {result.stdout}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Reliability: Substring JSON matching is brittle

run_http_check uses Python's in operator for substring matching against JSON responses (e.g., '"status":"ok"'). Go's json.Marshal produces compact JSON without spaces by default, but this is an implementation detail — different Go versions or Gin configurations could include whitespace, and the check would silently fail.

Additionally, a response like {"error":"'status':'ok' not found"} would falsely pass the "status":"ok" substring check since there's no JSON structure validation.

Consider parsing the response as JSON and comparing specific fields, or at minimum make the expected string matching more resilient (e.g., strip whitespace, use regex).

Comment on lines +225 to +248
result = sandbox.commands.run(
"pkill -f '^/tmp/gin-app$' >/dev/null 2>&1 || true; "
"rm -f /tmp/gin.log; "
"nohup /tmp/gin-app > /tmp/gin.log 2>&1 & "
"app_pid=$!; "
"i=0; "
"while [ $i -lt 20 ]; do "
" if curl -fsS --max-time 2 http://127.0.0.1:8080/health >/dev/null 2>&1; then "
" echo \"server ready, pid=${app_pid}\"; "
" exit 0; "
" fi; "
" if ! kill -0 ${app_pid} >/dev/null 2>&1; then "
" echo \"server process exited before becoming ready\"; "
" cat /tmp/gin.log || true; "
" exit 1; "
" fi; "
" i=$((i + 1)); "
" sleep 0.5; "
"done; "
"echo \"server failed to become ready within 10 seconds\"; "
"cat /tmp/gin.log || true; "
"exit 1",
timeout=30,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability: 24-line shell one-liner is hard to read and maintain

The server startup logic packs process management (pkill, nohup), a polling loop, health check, error recovery, and log dump into a single command string. This is hard to read, hard to debug, and hard to modify.

Consider either:

  1. Extracting this into a small shell script file (e.g., start_and_wait.sh) and calling sandbox.commands.run("./start_and_wait.sh")
  2. Splitting into separate sandbox.commands.run() calls: start the process, then poll for readiness in Python with time.sleep()
Comment on lines +37 to +45
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
wget \
git \
tar \
procps \
&& rm -rf /var/lib/apt/lists/*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproducibility: Apt packages not version-pinned

The apt-get install command (lines 37-45) does not pin package versions. Each docker build pulls whatever version the Debian repo currently serves, making builds non-reproducible. If using the base image tag :latest, a future docker build could silently pick up different package versions — or worse, a compromised package.

Consider either:

  1. Pinning the base image to a specific digest rather than :latest
  2. Pinning apt package versions (e.g., curl=7.88.1-10+deb12u8)
Signed-off-by: hyy321 <1970476830@qq.com>
@hyy321 hyy321 force-pushed the add-gin-api-sandbox-example branch from 298cea0 to 7e0de6b Compare July 1, 2026 04:04
@hyy321

hyy321 commented Jul 1, 2026

Copy link
Copy Markdown
Author

Updated the PR based on the review comments.

Main changes:

  • Run the image as the existing non-root user from the CubeSandbox base image.
  • Clarify that port 49999 is reserved for the CubeSandbox template health probe.
  • Pin apt package versions for a more reproducible image build.
  • Add SHA256 verification for the downloaded Go tarball before extraction.
  • Switch the Go toolchain download to curl -fsSL with retry and timeout options, and remove the unused wget dependency.
  • Make the extra file-write verification opt-in via VERIFY_FILE_WRITES=1 to avoid unnecessary sandbox round-trips.
  • Parse JSON responses in endpoint checks and compare specific fields instead of relying on substring matching.
  • Refactor the Gin server startup logic into a Python helper for better readability.
  • Load .env relative to the script directory.
  • Add a main() guard and avoid import-time side effects.

I also re-tested the updated example successfully:

  • Docker image build passed.
  • The 49999 /health template probe returned 200 OK.
  • The full SDK example completed successfully, including file writes, go mod tidy, go build, starting the Gin server, and all endpoint checks.

@chenhengqi chenhengqi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need this example.

If you want to get involved, consider helping us refactor CubeMaster with gin:

func HttpHandler(w http.ResponseWriter, r *http.Request) {
rt := CubeLog.GetTraceInfo(r.Context())
var rsp interface{}
switch {

@hyy321

hyy321 commented Jul 1, 2026

Copy link
Copy Markdown
Author

Got it, thanks for the suggestion. I’ll take a look at CubeMaster/pkg/service/httpservice/cube/cube.go and try to refactor the existing net/http style handlers into Gin routes. I’ll open a draft PR after I understand the current routing logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants