From dcfc4112a5c2c1adefa26572c3a7cf7f87a89e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E6=99=94=E5=A1=AC?= <2776079516@qq.com> Date: Mon, 6 Jul 2026 20:35:38 +0800 Subject: [PATCH] feat(examples): add Go and Java sandbox templates Add pre-compiled sandbox templates with tests: cubesandbox-base-go/ - Go HTTP server with test_sandbox.py integration test - main_test.go unit tests cubesandbox-base-java/ - Java HTTP server (HelloWorldServer) with test_sandbox.py - HelloWorldServerTest.java unit tests Each template includes Dockerfile, env config, and e2b SDK test scripts. --- docs/guide/tutorials/bring-your-own-image.md | 8 +- docs/guide/tutorials/examples.md | 2 + .../guide/tutorials/bring-your-own-image.md | 7 +- docs/zh/guide/tutorials/examples.md | 2 + examples/cubesandbox-base-go/Dockerfile | 49 +++++ examples/cubesandbox-base-go/README.md | 153 +++++++++++++++ examples/cubesandbox-base-go/env.example | 11 ++ examples/cubesandbox-base-go/main.go | 50 +++++ examples/cubesandbox-base-go/main_test.go | 98 ++++++++++ examples/cubesandbox-base-go/requirements.txt | 2 + examples/cubesandbox-base-go/test_sandbox.py | 37 ++++ examples/cubesandbox-base-java/Dockerfile | 45 +++++ .../HelloWorldServer.java | 63 +++++++ .../HelloWorldServerTest.java | 177 ++++++++++++++++++ examples/cubesandbox-base-java/README.md | 156 +++++++++++++++ examples/cubesandbox-base-java/env.example | 11 ++ .../cubesandbox-base-java/requirements.txt | 2 + .../cubesandbox-base-java/test_sandbox.py | 38 ++++ 18 files changed, 906 insertions(+), 5 deletions(-) create mode 100644 examples/cubesandbox-base-go/Dockerfile create mode 100644 examples/cubesandbox-base-go/README.md create mode 100644 examples/cubesandbox-base-go/env.example create mode 100644 examples/cubesandbox-base-go/main.go create mode 100644 examples/cubesandbox-base-go/main_test.go create mode 100644 examples/cubesandbox-base-go/requirements.txt create mode 100644 examples/cubesandbox-base-go/test_sandbox.py create mode 100644 examples/cubesandbox-base-java/Dockerfile create mode 100644 examples/cubesandbox-base-java/HelloWorldServer.java create mode 100644 examples/cubesandbox-base-java/HelloWorldServerTest.java create mode 100644 examples/cubesandbox-base-java/README.md create mode 100644 examples/cubesandbox-base-java/env.example create mode 100644 examples/cubesandbox-base-java/requirements.txt create mode 100644 examples/cubesandbox-base-java/test_sandbox.py diff --git a/docs/guide/tutorials/bring-your-own-image.md b/docs/guide/tutorials/bring-your-own-image.md index 0c1735cce..fd5018aae 100644 --- a/docs/guide/tutorials/bring-your-own-image.md +++ b/docs/guide/tutorials/bring-your-own-image.md @@ -40,8 +40,12 @@ template**. > Prefer to read a runnable end-to-end example? See > [`examples/cubesandbox-base-nginx`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-nginx) -> in the repo — a minimal demo that stacks nginx on top of -> `cubesandbox-base`. +> (nginx on `cubesandbox-base`), +> [`examples/cubesandbox-base-java`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-java) +> (OpenJDK 17 + Maven on `cubesandbox-base`), or +> [`examples/cubesandbox-base-go`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-go) +> (Go toolchain on `cubesandbox-base`) in the repo — all are minimal demos +> that add a real service on top of the base image. ### 2.1 Write a Dockerfile diff --git a/docs/guide/tutorials/examples.md b/docs/guide/tutorials/examples.md index 4e8b711ab..70fc38064 100644 --- a/docs/guide/tutorials/examples.md +++ b/docs/guide/tutorials/examples.md @@ -11,6 +11,8 @@ Hands-on examples demonstrating various Cube Sandbox use cases. Each example is | [OpenAI Agents SDK Integration](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/openai-agents-example) | Wire OpenAI Agents SDK's `E2BSandboxClient` to Cube Sandbox. Ships a minimal Shell Agent with Pause/Resume and a full SWE-bench Django debugging agent with streaming + tracing. | | [OpenAI Agents + Code Interpreter](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/openai-agents-code-interpreter) | Data-analysis Agent running pandas / matplotlib inside a Cube Sandbox. Provides two variants: generic E2B write+exec and Jupyter-kernel Code Interpreter with cross-turn state and auto image capture. | | [cube-bench](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/cube-bench) | CLI benchmark tool written in Go that measures sandbox creation/deletion latency at configurable concurrency levels. Features a real-time TUI dashboard (Bubbletea/Lipgloss), percentile report (P50/P95/P99), and JSON export. | +| [Java Runtime Template (cubesandbox-base-java)](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/cubesandbox-base-java) | A Cube-ready image stacking OpenJDK 17 LTS + Maven and a tiny JDK-only HTTP server on `cubesandbox-base`. Use it as a starting point for Java code-execution or Java Web-service sandboxes. | +| [Go Runtime Template (cubesandbox-base-go)](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/cubesandbox-base-go) | A Cube-ready image stacking the Go toolchain and a tiny standard-library HTTP server on `cubesandbox-base`. Use it as a starting point for Go code-execution or Go Web-service sandboxes. | ::: tip All examples share the same environment variable conventions (`E2B_API_URL`, `E2B_API_KEY`, `CUBE_TEMPLATE_ID`). See the [Quick Start](../quickstart.md) guide to set up your Cube Sandbox deployment first. diff --git a/docs/zh/guide/tutorials/bring-your-own-image.md b/docs/zh/guide/tutorials/bring-your-own-image.md index 089857f3c..ccd808042 100644 --- a/docs/zh/guide/tutorials/bring-your-own-image.md +++ b/docs/zh/guide/tutorials/bring-your-own-image.md @@ -34,9 +34,10 @@ Cube-Sandbox 与沙箱容器之间的所有通信都是通过容器内的 `envd` 了 `envd`,并附带一个通用入口脚本——后台拉起 `envd`、前台 `exec` 你 提供的 `CMD`。你只需要三步:**写 Dockerfile → 构建推送 → 创建模板**。 -> 想看一个能直接跑通的完整示例?可以参考仓库里的 -> [`examples/cubesandbox-base-nginx`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-nginx), -> 里面是把 nginx 叠在 `cubesandbox-base` 上的最小 demo。 +> 想看能直接跑通的完整示例?可以参考仓库里的 +> [`examples/cubesandbox-base-nginx`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-nginx)(nginx 叠在 `cubesandbox-base` 上)、 +> [`examples/cubesandbox-base-java`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-java)(OpenJDK 17 + Maven 叠在 `cubesandbox-base` 上)或 +> [`examples/cubesandbox-base-go`](https://github.com/TencentCloud/CubeSandbox/tree/master/examples/cubesandbox-base-go)(Go 工具链叠在 `cubesandbox-base` 上),都是在基础镜像上加一个真实服务的最小 demo。 ### 2.1 写 Dockerfile diff --git a/docs/zh/guide/tutorials/examples.md b/docs/zh/guide/tutorials/examples.md index 5ee2742b2..631272766 100644 --- a/docs/zh/guide/tutorials/examples.md +++ b/docs/zh/guide/tutorials/examples.md @@ -11,6 +11,8 @@ | [OpenAI Agents SDK 集成](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/openai-agents-example) | 将 OpenAI Agents SDK 的 `E2BSandboxClient` 对接 Cube Sandbox。包含最小 Shell Agent(含 Pause/Resume 演示)以及完整的 SWE-bench Django 调试 Agent(流式输出 + 全链路追踪)。 | | [OpenAI Agents + Code Interpreter](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/openai-agents-code-interpreter) | 在 Cube Sandbox 中运行使用 pandas / matplotlib 的数据分析 Agent,提供通用 E2B(write+exec)与 Jupyter kernel(状态跨轮保留、图像自动捕获)两种执行形态。 | | [cube-bench](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/cube-bench) | Go 编写的 CLI 压测工具,可在可配置并发数下测量沙箱创建/删除延迟。具备实时 TUI 看板(Bubbletea/Lipgloss)、分位数报告(P50/P95/P99)和 JSON 导出功能。 | +| [Java 运行时模板(cubesandbox-base-java)](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/cubesandbox-base-java) | 在 `cubesandbox-base` 上叠加 OpenJDK 17 LTS + Maven 与一个仅依赖 JDK 的极简 HTTP 服务器。可作为 Java 代码执行或 Java Web 服务沙箱的起点。 | +| [Go 运行时模板(cubesandbox-base-go)](https://github.com/tencentcloud/CubeSandbox/tree/master/examples/cubesandbox-base-go) | 在 `cubesandbox-base` 上叠加 Go 工具链与一个仅用标准库的极简 HTTP 服务器。可作为 Go 代码执行或 Go Web 服务沙箱的起点。 | ::: tip 所有示例共享相同的环境变量约定(`E2B_API_URL`、`E2B_API_KEY`、`CUBE_TEMPLATE_ID`)。请先参考[快速开始](../quickstart.md)指南搭建 Cube Sandbox 环境。 diff --git a/examples/cubesandbox-base-go/Dockerfile b/examples/cubesandbox-base-go/Dockerfile new file mode 100644 index 000000000..0d42fd8f6 --- /dev/null +++ b/examples/cubesandbox-base-go/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1.7 +# +# Demo: a Cube-ready sandbox image that runs a tiny Go HTTP server on :8080 +# while the cubesandbox-base entrypoint keeps envd running on :49983 in the +# background. A good starting point for Go workloads in CubeSandbox. +# +# Build: +# docker build -t cubesandbox-demo-go:latest examples/cubesandbox-base-go +# +# Run locally: +# docker run --rm -d -p 8080:8080 -p 49983:49983 \ +# --name cube-demo-go cubesandbox-demo-go:latest +# curl http://127.0.0.1:8080/ +# curl -o /dev/null -w "%{http_code}\n" http://127.0.0.1:49983/health # => 204 + +ARG CUBE_BASE_IMAGE=ghcr.io/tencentcloud/cubesandbox-base:2026.16 +FROM ${CUBE_BASE_IMAGE} + +ARG DEBIAN_FRONTEND=noninteractive +ARG GO_VERSION=1.23.4 + +# Install the Go toolchain from the official tarball — newer than Ubuntu +# 22.04's apt golang (1.18) and self-contained under /usr/local/go. +RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tgz \ + && tar -C /usr/local -xzf /tmp/go.tgz \ + && rm /tmp/go.tgz \ + && /usr/local/go/bin/go version + +ENV GOROOT=/usr/local/go \ + GOPATH=/go \ + PATH="/usr/local/go/bin:/go/bin:${PATH}" + +WORKDIR /app +COPY main.go /app/main.go +COPY main_test.go /app/main_test.go + +# Run unit tests (httptest-based, no network needed) before building. +RUN CGO_ENABLED=0 go test -v . + +# Build a static-ish binary; CGO_ENABLED=0 avoids libc linkage surprises. +RUN CGO_ENABLED=0 go build -o /app/helloserver main.go + +EXPOSE 8080 49983 + +# cube-entrypoint.sh (inherited from the base image) will: +# 1. Launch envd on ${ENVD_PORT:-49983} in the background. +# 2. exec the CMD below as the foreground process, so the Go server owns +# PID 1's child and receives SIGTERM on `docker stop`. +CMD ["/app/helloserver"] diff --git a/examples/cubesandbox-base-go/README.md b/examples/cubesandbox-base-go/README.md new file mode 100644 index 000000000..f50ef4a58 --- /dev/null +++ b/examples/cubesandbox-base-go/README.md @@ -0,0 +1,153 @@ +# cubesandbox-base-go demo + +A minimal image that stacks a **Go toolchain** and a tiny HTTP server on top of +[`cubesandbox-base`](../../docker/Dockerfile.cube-base), so you can test the +"Bring Your Own Image" flow end-to-end with a real Go workload — and use it as +a ready-made starting point for Go-based sandboxes. + +- envd listens on `:49983` (Cube readiness probe) — inherited from the base image. +- A Go HTTP server (standard library `net/http`) listens on `:8080` and serves a + tiny landing page that echoes the Go runtime version, so you can eyeball that + Go really served the request. + +See [Bring Your Own Image (envd)](../../docs/guide/tutorials/bring-your-own-image.md) +for the full tutorial, and [`../cubesandbox-base-nginx/`](../cubesandbox-base-nginx/) +for a sibling example that follows the exact same pattern with nginx. + +## What's inside + +- **Go toolchain** — official tarball from `go.dev`, newer than Ubuntu 22.04's + apt `golang` (1.18). Installed under `/usr/local/go`, with `GOROOT`/`GOPATH`/`PATH` + wired up so `go` is on `$PATH`. +- **`main.go`** — a single-file HTTP server built on the standard library + `net/http` package, so the image needs **no third-party dependencies** and no + network access to build. Compiled to `/app/helloserver` at build time and + launched as the foreground process via `CMD ["/app/helloserver"]`. + +## Build + +```bash +docker build -t cubesandbox-demo-go:latest . +``` + +The Dockerfile runs `go test` during the build, so a failing unit test +will abort the image build. + +> Pin a different Go version with `--build-arg GO_VERSION=1.22.0`. + +## Run unit tests locally + +```bash +go test -v . +``` + +Tests use only the standard library `net/http/httptest` — no external +dependencies or running server required. + +## Run & verify locally + +```bash +docker run --rm -d \ + -p 8080:8080 \ + -p 49983:49983 \ + --name cube-demo-go \ + cubesandbox-demo-go:latest + +# Go server: should print the demo landing page (with the Go runtime version) +curl -s http://127.0.0.1:8080/ + +# envd readiness probe: should return 204 +curl -s -o /dev/null -w "envd /health => %{http_code}\n" \ + http://127.0.0.1:49983/health + +# Sanity-check the toolchain inside the container +docker exec cube-demo-go go version +docker exec cube-demo-go go env GOROOT GOPATH + +docker rm -f cube-demo-go +``` + +## Register as a Cube template + +```bash +cubemastercli tpl create-from-image \ + --image /cubesandbox-demo-go:latest \ + --writable-layer-size 1G \ + --expose-port 49983 \ + --expose-port 8080 \ + --probe 49983 \ + --probe-path /health +``` + +`--probe 49983 --probe-path /health` points Cube at envd (guaranteed to +return `204` within ~1s); the Go server's `:8080` stays exposed for your +actual traffic. See +[Creating Templates from OCI Images](../../docs/guide/tutorials/template-from-image.md) +for monitoring (`tpl watch`) and troubleshooting. + +## Try it with the E2B SDK + +After registering the template, [`test_sandbox.py`](./test_sandbox.py) boots +a sandbox from it and verifies four things: + +1. `go version` runs inside the sandbox (toolchain is on `$PATH`) +2. `go env GOROOT GOPATH` reports the expected paths +3. `/app/main.go` is readable via `sandbox.files.read(...)` +4. An HTTPS request to the sandbox's port `8080` returns the Go server's + landing page + +```bash +pip install -r requirements.txt + +cp env.example .env +# fill in E2B_API_URL and CUBE_TEMPLATE_ID + +python3 test_sandbox.py +``` + +## Files + +``` +cubesandbox-base-go/ +├── Dockerfile # FROM cubesandbox-base, installs Go toolchain, builds & runs the server +├── main.go # Standard-library HTTP server (net/http), no third-party deps +├── main_test.go # Unit tests for handleRoot / handleHealth (go test, no external deps) +├── test_sandbox.py # E2B SDK smoke test: go version/env, file read, HTTP GET :8080 +├── env.example # E2B_API_URL / E2B_API_KEY / CUBE_TEMPLATE_ID +├── requirements.txt # e2b + python-dotenv +└── README.md # This file +``` + +## Use cases + +- **Go code execution sandbox** — boot a sandbox and run `go build` / `go test` + via `sandbox.commands.run(...)` for isolated build or test jobs. +- **Go Web service base** — replace `main.go` with your own Gin / Echo / chi / + vanilla `net/http` app and expose its port with `--expose-port`. +- **CGo / multi-module build runner** — clone a repo into the sandbox and run + `go build ./...` against an isolated, disposable environment. + +## Known limitations + +- The image ships a **single Go version** (default `1.23.4`). Override with + `--build-arg GO_VERSION=` for a different release. +- **`GOPATH` is `/go`** with no pre-created `src` tree. Go modules are the + default and recommended workflow; if you need legacy `GOPATH` mode, `mkdir -p + /go/src` in your downstream Dockerfile. +- `main.go` is a demo, not a production server — it has no TLS, no graceful + shutdown, no timeouts, and serves a single route. Swap it for your real + application's binary. +- The Go binary is built with `CGO_ENABLED=0` (pure Go). If your project needs + CGo (e.g. links a C library), drop that flag and ensure the toolchain image + has a C compiler (`apt-get install -y gcc`). + +## Related + +- [`../cubesandbox-base-nginx/`](../cubesandbox-base-nginx/) — the sibling + example this template is modelled on (nginx instead of Go). +- [`../cubesandbox-base-java/`](../cubesandbox-base-java/) — another sibling, + Java 17 + Maven on the same base image. +- [Bring Your Own Image (envd)](../../docs/guide/tutorials/bring-your-own-image.md) + — the entrypoint contract and `envd` requirements. +- [Creating Templates from OCI Images](../../docs/guide/tutorials/template-from-image.md) + — full `cubemastercli tpl create-from-image` reference. diff --git a/examples/cubesandbox-base-go/env.example b/examples/cubesandbox-base-go/env.example new file mode 100644 index 000000000..e2da06360 --- /dev/null +++ b/examples/cubesandbox-base-go/env.example @@ -0,0 +1,11 @@ +# Required: Cube API server address +export E2B_API_URL="http://:3000" + +# Required: any non-empty value satisfies the SDK check +export E2B_API_KEY="e2b_000000" + +# Required: template ID from `cubemastercli tpl create-from-image ...` +export CUBE_TEMPLATE_ID="" + + +export SSL_CERT_FILE="/etc/pki/tls/cert.pem" diff --git a/examples/cubesandbox-base-go/main.go b/examples/cubesandbox-base-go/main.go new file mode 100644 index 000000000..b9a0effa2 --- /dev/null +++ b/examples/cubesandbox-base-go/main.go @@ -0,0 +1,50 @@ +// Minimal HTTP server used by the cubesandbox-base-go demo template. +// +// Uses only the Go standard library (net/http) so the image needs no +// third-party dependencies. Listens on :8080 (override via APP_PORT) and +// serves a tiny landing page plus a /health endpoint. The Cube readiness +// probe is served by envd on :49983 — this server is the "real" application +// traffic endpoint, mirroring how nginx serves :80 in cubesandbox-base-nginx. +package main + +import ( + "fmt" + "net/http" + "os" + "runtime" +) + +func main() { + port := os.Getenv("APP_PORT") + if port == "" { + port = "8080" + } + + mux := http.NewServeMux() + mux.HandleFunc("/", handleRoot) + mux.HandleFunc("/health", handleHealth) + + fmt.Printf("helloserver listening on :%s (Go %s)\n", port, runtime.Version()) + if err := http.ListenAndServe(":"+port, mux); err != nil { + fmt.Fprintln(os.Stderr, "server error:", err) + os.Exit(1) + } +} + +func handleRoot(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + port := os.Getenv("APP_PORT") + if port == "" { + port = "8080" + } + fmt.Fprintf(w, "\n"+ + "cubesandbox-base-go\n"+ + "

Hello from Go inside a CubeSandbox MicroVM

\n"+ + "

Go runtime: %s

\n"+ + "

envd is running on :49983, this Go server on :%s.

\n", + runtime.Version(), port) +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/examples/cubesandbox-base-go/main_test.go b/examples/cubesandbox-base-go/main_test.go new file mode 100644 index 000000000..0dab9905a --- /dev/null +++ b/examples/cubesandbox-base-go/main_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHandleRootReturnsOKWithExpectedContent(t *testing.T) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + handleRoot(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("handleRoot status = %d, want %d", rr.Code, http.StatusOK) + } + + ct := rr.Header().Get("Content-Type") + if ct != "text/html; charset=utf-8" { + t.Fatalf("Content-Type = %q, want %q", ct, "text/html; charset=utf-8") + } + + body := rr.Body.String() + for _, want := range []string{ + "cubesandbox-base-go", + "Hello from Go inside a CubeSandbox MicroVM", + "Go runtime:", + } { + if !strings.Contains(body, want) { + t.Errorf("response body missing %q\nbody:\n%s", want, body) + } + } +} + +func TestHandleRootEchoesAppPortFromEnv(t *testing.T) { + t.Setenv("APP_PORT", "9999") + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + handleRoot(rr, req) + + if !strings.Contains(rr.Body.String(), ":9999") { + t.Errorf("body should contain the APP_PORT value 9999\nbody:\n%s", rr.Body.String()) + } +} + +func TestHandleRootDefaultsToPort8080(t *testing.T) { + t.Setenv("APP_PORT", "") + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + handleRoot(rr, req) + + if !strings.Contains(rr.Body.String(), ":8080") { + t.Errorf("body should contain default port 8080\nbody:\n%s", rr.Body.String()) + } +} + +func TestHandleHealthReturnsOKWithEmptyBody(t *testing.T) { + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/health", nil) + + handleHealth(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("handleHealth status = %d, want %d", rr.Code, http.StatusOK) + } + + body, err := io.ReadAll(rr.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + if len(body) != 0 { + t.Fatalf("handleHealth body = %q, want empty", body) + } +} + +func TestHandleHealthIgnoresMethod(t *testing.T) { + for _, method := range []string{ + http.MethodGet, + http.MethodPost, + http.MethodHead, + } { + rr := httptest.NewRecorder() + req := httptest.NewRequest(method, "/health", nil) + + handleHealth(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("method %s: status = %d, want %d", method, rr.Code, http.StatusOK) + } + } +} diff --git a/examples/cubesandbox-base-go/requirements.txt b/examples/cubesandbox-base-go/requirements.txt new file mode 100644 index 000000000..ecae38fb7 --- /dev/null +++ b/examples/cubesandbox-base-go/requirements.txt @@ -0,0 +1,2 @@ +e2b>=2.4.1 +python-dotenv diff --git a/examples/cubesandbox-base-go/test_sandbox.py b/examples/cubesandbox-base-go/test_sandbox.py new file mode 100644 index 000000000..4830824fc --- /dev/null +++ b/examples/cubesandbox-base-go/test_sandbox.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +import os +import urllib.request +from pathlib import Path + +from dotenv import load_dotenv +from e2b import Sandbox + +for candidate in (Path(__file__).with_name(".env"), Path.cwd() / ".env"): + if candidate.is_file(): + load_dotenv(dotenv_path=candidate, override=False) + break + +template_id = os.environ["CUBE_TEMPLATE_ID"] + +with Sandbox.create(template=template_id) as sandbox: + print("=== go version ===") + r = sandbox.commands.run("go version") + print(r.stdout) + print("exit_code:", r.exit_code) + + print("=== GOPATH / GOROOT ===") + r = sandbox.commands.run("go env GOROOT GOPATH") + print(r.stdout) + print("exit_code:", r.exit_code) + + print("=== read /app/main.go ===") + print(sandbox.files.read("/app/main.go", user="root")) + + print("=== GET https://:8080/ ===") + url = f"https://{sandbox.get_host(8080)}/" + print(f"url: {url}") + with urllib.request.urlopen(url, timeout=10) as resp: + print(f"status: {resp.status}") + print(resp.read().decode()) diff --git a/examples/cubesandbox-base-java/Dockerfile b/examples/cubesandbox-base-java/Dockerfile new file mode 100644 index 000000000..b9a716419 --- /dev/null +++ b/examples/cubesandbox-base-java/Dockerfile @@ -0,0 +1,45 @@ +# syntax=docker/dockerfile:1.7 +# +# Demo: a Cube-ready sandbox image that runs a tiny Java HTTP server on :8080 +# while the cubesandbox-base entrypoint keeps envd running on :49983 in the +# background. A good starting point for Java workloads in CubeSandbox. +# +# Build: +# docker build -t cubesandbox-demo-java:latest examples/cubesandbox-base-java +# +# Run locally: +# docker run --rm -d -p 8080:8080 -p 49983:49983 \ +# --name cube-demo-java cubesandbox-demo-java:latest +# curl http://127.0.0.1:8080/ +# curl -o /dev/null -w "%{http_code}\n" http://127.0.0.1:49983/health # => 204 + +ARG CUBE_BASE_IMAGE=ghcr.io/tencentcloud/cubesandbox-base:2026.16 +FROM ${CUBE_BASE_IMAGE} + +ARG DEBIAN_FRONTEND=noninteractive + +# Java 17 LTS (headless) + Maven for the most common Java toolchain. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + openjdk-17-jdk-headless \ + maven \ + && rm -rf /var/lib/apt/lists/* + +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \ + PATH="${JAVA_HOME}/bin:${PATH}" + +WORKDIR /app +COPY HelloWorldServer.java /app/HelloWorldServer.java +COPY HelloWorldServerTest.java /app/HelloWorldServerTest.java + +# Compile both the server and its test, then run the test suite. +RUN javac HelloWorldServer.java HelloWorldServerTest.java +RUN java HelloWorldServerTest + +EXPOSE 8080 49983 + +# cube-entrypoint.sh (inherited from the base image) will: +# 1. Launch envd on ${ENVD_PORT:-49983} in the background. +# 2. exec the CMD below as the foreground process, so the Java server owns +# PID 1's child and receives SIGTERM on `docker stop`. +CMD ["java", "HelloWorldServer"] diff --git a/examples/cubesandbox-base-java/HelloWorldServer.java b/examples/cubesandbox-base-java/HelloWorldServer.java new file mode 100644 index 000000000..fc762b0e5 --- /dev/null +++ b/examples/cubesandbox-base-java/HelloWorldServer.java @@ -0,0 +1,63 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; + +/** + * Minimal HTTP server used by the cubesandbox-base-java demo template. + * + * Uses only the JDK built-in {@code com.sun.net.httpserver} API so the image + * needs no third-party dependencies. Listens on :8080 (override via APP_PORT) + * and serves a tiny landing page plus a /health endpoint. The Cube readiness + * probe is served by envd on :49983 — this server is the "real" application + * traffic endpoint, mirroring how nginx serves :80 in cubesandbox-base-nginx. + */ +public class HelloWorldServer { + + public static void main(String[] args) throws IOException { + int port = Integer.parseInt(System.getenv().getOrDefault("APP_PORT", "8080")); + HttpServer server = createServer(port); + server.start(); + System.out.println("HelloWorldServer listening on :" + port + + " (JVM " + System.getProperty("java.version") + ")"); + } + + /** + * Creates and configures (but does not start) the HTTP server. + * + * Exposed as package-private so that {@link HelloWorldServerTest} can bind + * to port 0 (OS-assigned), start the server, fire requests, and shut it + * down cleanly — without touching the real {@code APP_PORT}. + */ + static HttpServer createServer(int port) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/", HelloWorldServer::handleRoot); + server.createContext("/health", HelloWorldServer::handleHealth); + server.setExecutor(Executors.newFixedThreadPool(4)); + return server; + } + + private static void handleRoot(HttpExchange exchange) throws IOException { + String body = "\n" + + "cubesandbox-base-java\n" + + "

Hello from Java inside a CubeSandbox MicroVM

\n" + + "

JVM: " + System.getProperty("java.version") + "

\n" + + "

envd is running on :49983, this Java server on :" + + System.getenv().getOrDefault("APP_PORT", "8080") + ".

\n"; + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + private static void handleHealth(HttpExchange exchange) throws IOException { + exchange.sendResponseHeaders(200, -1); + exchange.close(); + } +} diff --git a/examples/cubesandbox-base-java/HelloWorldServerTest.java b/examples/cubesandbox-base-java/HelloWorldServerTest.java new file mode 100644 index 000000000..d27055c46 --- /dev/null +++ b/examples/cubesandbox-base-java/HelloWorldServerTest.java @@ -0,0 +1,177 @@ +import com.sun.net.httpserver.HttpServer; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Standalone test for {@link HelloWorldServer}. + * + * Uses only JDK built-in APIs (no JUnit) to stay consistent with the + * zero-dependency philosophy of the template. Run with: + * + *
+ *   javac HelloWorldServer.java HelloWorldServerTest.java
+ *   java HelloWorldServerTest
+ * 
+ * + * Exit code 0 = all tests passed; non-zero = at least one failure. + */ +public class HelloWorldServerTest { + + private static int failures = 0; + + public static void main(String[] args) throws Exception { + HttpServer server = HelloWorldServer.createServer(0); // port 0 → OS picks a free port + server.start(); + int port = server.getAddress().getPort(); + String base = "http://127.0.0.1:" + port; + + HttpClient client = HttpClient.newBuilder() + .connectTimeout(java.time.Duration.ofSeconds(5)) + .build(); + + try { + testRootReturnsOkWithExpectedContent(client, base); + testRootContentTypeIsHtmlUtf8(client, base); + testHealthReturnsOk(client, base); + testConcurrentRequestsAllSucceed(client, base); + } finally { + server.stop(0); + } + + if (failures > 0) { + System.err.println("\n" + failures + " test(s) FAILED"); + System.exit(1); + } else { + System.out.println("\nAll tests passed"); + } + } + + // ------------------------------------------------------------------ + // Individual tests + // ------------------------------------------------------------------ + + private static void testRootReturnsOkWithExpectedContent(HttpClient client, String base) + throws Exception { + HttpResponse resp = sendGet(client, base + "/"); + + assertEquals("root status", 200, resp.statusCode()); + + String body = resp.body(); + assertContains("root body", body, "Hello from Java inside a CubeSandbox MicroVM"); + assertContains("root body", body, "cubesandbox-base-java"); + assertContains("root body", body, "JVM:"); + } + + private static void testRootContentTypeIsHtmlUtf8(HttpClient client, String base) + throws Exception { + HttpResponse resp = sendGet(client, base + "/"); + + String ct = resp.headers().firstValue("Content-Type").orElse(""); + assertEquals("root Content-Type", "text/html; charset=utf-8", ct); + } + + private static void testHealthReturnsOk(HttpClient client, String base) + throws Exception { + HttpResponse resp = sendGet(client, base + "/health"); + + assertEquals("health status", 200, resp.statusCode()); + } + + /** + * Fires 32 concurrent GET / requests and verifies that every one returns + * 200. This directly exercises the thread-pool executor — with the old + * {@code setExecutor(null)} (single-threaded dispatcher) the test still + * passes but requests are serialised; with the fixed thread pool they are + * handled in parallel. + */ + private static void testConcurrentRequestsAllSucceed(HttpClient client, String base) + throws Exception { + final int numRequests = 32; + ExecutorService pool = Executors.newFixedThreadPool(numRequests); + CountDownLatch ready = new CountDownLatch(1); + CountDownLatch done = new CountDownLatch(numRequests); + + List>> futures = new ArrayList<>(numRequests); + for (int i = 0; i < numRequests; i++) { + futures.add(pool.submit(() -> { + ready.await(); // all threads start together + HttpResponse r = sendGet(client, base + "/"); + done.countDown(); + return r; + })); + } + + ready.countDown(); // release all threads at once + boolean finished = done.await(15, TimeUnit.SECONDS); + pool.shutdown(); + + if (!finished) { + fail("concurrent: not all requests completed within 15s"); + return; + } + + int ok = 0; + for (Future> f : futures) { + HttpResponse r = f.get(); + if (r.statusCode() == 200) { + ok++; + } + } + + assertEquals("concurrent requests that returned 200", numRequests, ok); + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private static HttpResponse sendGet(HttpClient client, String url) + throws Exception { + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(java.time.Duration.ofSeconds(10)) + .GET() + .build(); + return client.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } + + private static void assertEquals(String label, int expected, int actual) { + if (expected != actual) { + fail(label + ": expected " + expected + ", got " + actual); + } else { + System.out.println(" [OK] " + label + " = " + actual); + } + } + + private static void assertEquals(String label, String expected, String actual) { + if (!expected.equals(actual)) { + fail(label + ": expected \"" + expected + "\", got \"" + actual + "\""); + } else { + System.out.println(" [OK] " + label + " = \"" + actual + "\""); + } + } + + private static void assertContains(String label, String haystack, String needle) { + if (!haystack.contains(needle)) { + fail(label + ": missing substring \"" + needle + "\"\nbody:\n" + haystack); + } else { + System.out.println(" [OK] " + label + " contains \"" + needle + "\""); + } + } + + private static void fail(String message) { + failures++; + System.err.println(" [FAIL] " + message); + } +} diff --git a/examples/cubesandbox-base-java/README.md b/examples/cubesandbox-base-java/README.md new file mode 100644 index 000000000..faef7e875 --- /dev/null +++ b/examples/cubesandbox-base-java/README.md @@ -0,0 +1,156 @@ +# cubesandbox-base-java demo + +A minimal image that stacks a **Java 17 LTS + Maven** toolchain and a tiny +HTTP server on top of +[`cubesandbox-base`](../../docker/Dockerfile.cube-base), so you can test the +"Bring Your Own Image" flow end-to-end with a real Java workload — and use +it as a ready-made starting point for Java-based sandboxes. + +- envd listens on `:49983` (Cube readiness probe) — inherited from the base image. +- A JDK-built HTTP server listens on `:8080` and serves a tiny landing page + that echoes the JVM version, so you can eyeball that Java really served + the request. + +See [Bring Your Own Image (envd)](../../docs/guide/tutorials/bring-your-own-image.md) +for the full tutorial, and [`../cubesandbox-base-nginx/`](../cubesandbox-base-nginx/) +for a sibling example that follows the exact same pattern with nginx. + +## What's inside + +- **OpenJDK 17 LTS** (`openjdk-17-jdk-headless`) — `java` / `javac` on `$PATH`. +- **Maven** — for building Java projects inside the sandbox. +- **`HelloWorldServer.java`** — a single-file HTTP server built on the JDK's + built-in `com.sun.net.httpserver` API, so the image needs **no third-party + dependencies**. Compiled to `/app/HelloWorldServer.class` at build time and + launched as the foreground process via `CMD ["java", "HelloWorldServer"]`. + +## Build + +```bash +docker build -t cubesandbox-demo-java:latest . +``` + +The Dockerfile compiles and runs `HelloWorldServerTest` during the build, +so a failing test will abort the image build. + +## Run unit tests locally + +```bash +javac HelloWorldServer.java HelloWorldServerTest.java +java HelloWorldServerTest +``` + +Tests use only JDK built-in APIs (`java.net.http.HttpClient`, +`java.util.concurrent`) — no JUnit or other external dependencies required. +The concurrent-request test fires 32 simultaneous GET requests to verify +the thread-pool executor handles parallel load correctly. + +## Run & verify locally + +```bash +docker run --rm -d \ + -p 8080:8080 \ + -p 49983:49983 \ + --name cube-demo-java \ + cubesandbox-demo-java:latest + +# Java server: should print the demo landing page (with the JVM version) +curl -s http://127.0.0.1:8080/ + +# envd readiness probe: should return 204 +curl -s -o /dev/null -w "envd /health => %{http_code}\n" \ + http://127.0.0.1:49983/health + +# Sanity-check the toolchain inside the container +docker exec cube-demo-java java -version +docker exec cube-demo-java mvn -version + +docker rm -f cube-demo-java +``` + +## Register as a Cube template + +```bash +cubemastercli tpl create-from-image \ + --image /cubesandbox-demo-java:latest \ + --writable-layer-size 1G \ + --expose-port 49983 \ + --expose-port 8080 \ + --probe 49983 \ + --probe-path /health +``` + +`--probe 49983 --probe-path /health` points Cube at envd (guaranteed to +return `204` within ~1s); the Java server's `:8080` stays exposed for your +actual traffic. See +[Creating Templates from OCI Images](../../docs/guide/tutorials/template-from-image.md) +for monitoring (`tpl watch`) and troubleshooting. + +## Try it with the E2B SDK + +After registering the template, [`test_sandbox.py`](./test_sandbox.py) boots +a sandbox from it and verifies four things: + +1. `java -version` runs inside the sandbox (JDK is on `$PATH`) +2. `mvn -version` runs inside the sandbox (Maven is available) +3. `/app/HelloWorldServer.java` is readable via `sandbox.files.read(...)` +4. An HTTPS request to the sandbox's port `8080` returns the Java server's + landing page + +```bash +pip install -r requirements.txt + +cp env.example .env +# fill in E2B_API_URL and CUBE_TEMPLATE_ID + +python3 test_sandbox.py +``` + +## Files + +``` +cubesandbox-base-java/ +├── Dockerfile # FROM cubesandbox-base, installs JDK 17 + Maven, compiles & runs the server +├── HelloWorldServer.java # JDK-only HTTP server (com.sun.net.httpserver), no third-party deps +├── HelloWorldServerTest.java # Unit tests incl. concurrent-request test (JDK-only, no JUnit) +├── test_sandbox.py # E2B SDK smoke test: java/mvn version, file read, HTTP GET :8080 +├── env.example # E2B_API_URL / E2B_API_KEY / CUBE_TEMPLATE_ID +├── requirements.txt # e2b + python-dotenv +└── README.md # This file +``` + +## Use cases + +- **Java code execution sandbox** — boot a sandbox and run `java`/`javac`/`mvn` + via `sandbox.commands.run(...)` for isolated build or test jobs. +- **Java Web service base** — replace `HelloWorldServer` with your own + Spring Boot / Quarkus / vanilla servlet app and expose its port with + `--expose-port`. +- **Maven build runner** — clone a repo into the sandbox and run `mvn + clean package` against an isolated, disposable environment. + +## Known limitations + +- The image ships **JDK 17 LTS** only. For a different LTS (11 / 21) change + the `apt-get install` line in the `Dockerfile` and the `JAVA_HOME` env var. +- **Gradle is not preinstalled**; add `gradle` to the `apt-get` line if your + project needs it, or use the [Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) + (`./gradlew`) which needs no system install. +- `HelloWorldServer` is a demo, not a production server — it has no TLS, + no graceful shutdown, and serves a single route. It does use a fixed + thread pool (`Executors.newFixedThreadPool(4)`) so concurrent requests + are handled in parallel, but the pool size is not configurable without + editing the source. Swap it for your real application's `CMD`. +- The Java server runs as `root` (the container's default user, inherited + from the base image's `CMD` execution). For a hardened template, drop + privileges to the base image's `user` (uid 1000) account before starting + the JVM. + +## Related + +- [`../cubesandbox-base-nginx/`](../cubesandbox-base-nginx/) — the sibling + example this template is modelled on (nginx instead of Java). +- [Bring Your Own Image (envd)](../../docs/guide/tutorials/bring-your-own-image.md) + — the entrypoint contract and `envd` requirements. +- [Creating Templates from OCI Images](../../docs/guide/tutorials/template-from-image.md) + — full `cubemastercli tpl create-from-image` reference. diff --git a/examples/cubesandbox-base-java/env.example b/examples/cubesandbox-base-java/env.example new file mode 100644 index 000000000..e2da06360 --- /dev/null +++ b/examples/cubesandbox-base-java/env.example @@ -0,0 +1,11 @@ +# Required: Cube API server address +export E2B_API_URL="http://:3000" + +# Required: any non-empty value satisfies the SDK check +export E2B_API_KEY="e2b_000000" + +# Required: template ID from `cubemastercli tpl create-from-image ...` +export CUBE_TEMPLATE_ID="" + + +export SSL_CERT_FILE="/etc/pki/tls/cert.pem" diff --git a/examples/cubesandbox-base-java/requirements.txt b/examples/cubesandbox-base-java/requirements.txt new file mode 100644 index 000000000..ecae38fb7 --- /dev/null +++ b/examples/cubesandbox-base-java/requirements.txt @@ -0,0 +1,2 @@ +e2b>=2.4.1 +python-dotenv diff --git a/examples/cubesandbox-base-java/test_sandbox.py b/examples/cubesandbox-base-java/test_sandbox.py new file mode 100644 index 000000000..74999a5fd --- /dev/null +++ b/examples/cubesandbox-base-java/test_sandbox.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Tencent Inc. +# SPDX-License-Identifier: Apache-2.0 + +import os +import urllib.request +from pathlib import Path + +from dotenv import load_dotenv +from e2b import Sandbox + +for candidate in (Path(__file__).with_name(".env"), Path.cwd() / ".env"): + if candidate.is_file(): + load_dotenv(dotenv_path=candidate, override=False) + break + +template_id = os.environ["CUBE_TEMPLATE_ID"] + +with Sandbox.create(template=template_id) as sandbox: + print("=== java -version ===") + # java -version writes to stderr; merge so we can see it via stdout. + r = sandbox.commands.run("java -version 2>&1") + print(r.stdout) + print("exit_code:", r.exit_code) + + print("=== mvn -version ===") + r = sandbox.commands.run("mvn -version 2>&1") + print(r.stdout) + print("exit_code:", r.exit_code) + + print("=== read /app/HelloWorldServer.java ===") + print(sandbox.files.read("/app/HelloWorldServer.java", user="root")) + + print("=== GET https://:8080/ ===") + url = f"https://{sandbox.get_host(8080)}/" + print(f"url: {url}") + with urllib.request.urlopen(url, timeout=10) as resp: + print(f"status: {resp.status}") + print(resp.read().decode())