Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions sdk/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# cubesandbox Go SDK

Go SDK for [CubeSandbox](https://github.com/TencentCloud/CubeSandbox). It matches the current Python SDK surface: sandbox lifecycle, code execution, commands, and file reads only.

## Install

```bash
go get github.com/tencentcloud/CubeSandbox/sdk/go
```

## Configuration

```bash
export CUBE_API_URL=http://127.0.0.1:3000
export CUBE_TEMPLATE_ID=<your-template-id>

# Optional remote data-plane access.
export CUBE_PROXY_NODE_IP=<cubeproxy-node-ip>
export CUBE_PROXY_PORT_HTTP=80
export CUBE_PROXY_SCHEME=http
export CUBE_SANDBOX_DOMAIN=cube.app
```

`NewConfigFromEnv` also accepts `E2B_API_URL` and `E2B_API_KEY`; `CUBE_API_URL` and `CUBE_API_KEY` take precedence.
`CUBE_PROXY_SCHEME` supports `http` and `https`; when omitted, port `443` defaults to `https` and other ports default to `http`.

## Create And Run Code

```go
package main

import (
"context"
"fmt"

cubesandbox "github.com/tencentcloud/CubeSandbox/sdk/go"
)

func main() {
ctx := context.Background()
client := cubesandbox.NewClient(cubesandbox.NewConfigFromEnv())

sb, err := client.Create(ctx, cubesandbox.CreateOptions{})
if err != nil {
panic(err)
}
defer sb.Kill(ctx)

exec, err := sb.RunCode(ctx, "x = 41\nx + 1", cubesandbox.RunCodeOptions{})
if err != nil {
panic(err)
}
fmt.Println(exec.Text)
}
```

## Commands

```go
result, err := sb.Commands().Run(ctx, "echo hello", cubesandbox.CommandOptions{})
if err != nil {
panic(err)
}
fmt.Println(result.Stdout, result.Stderr, result.ExitCode)
```

`Commands.Run` starts `/bin/bash -l -c <command>` through envd's `process.Process/Start` API and returns stdout, stderr, and the `EndEvent` exit code. Callers are still responsible for treating untrusted shell input carefully.

## Files

```go
content, err := sb.Files().Read(ctx, "/etc/hosts")
```

`Files.Read` downloads content through envd's `GET /files?path=...` file API.

## Pause And Connect

```go
wait := true
if err := sb.Pause(ctx, cubesandbox.PauseOptions{Wait: &wait}); err != nil {
panic(err)
}

resumed, err := client.Connect(ctx, sb.SandboxID)
if err != nil {
panic(err)
}
_ = resumed
```

`Sandbox.Resume` is available for compatibility but deprecated; prefer `Client.Connect`.

## Network Policy

```go
denyInternet := false
sb, err := client.Create(ctx, cubesandbox.CreateOptions{
AllowInternetAccess: &denyInternet,
Network: cubesandbox.NetworkOptions{
AllowOut: []string{"151.101.0.0/16"},
DenyOut: []string{"0.0.0.0/0"},
},
})
```

## Host Directory Mount

```go
sb, err := client.Create(ctx, cubesandbox.CreateOptions{
Metadata: map[string]string{
"hostdir-mount": `[{"hostPath":"/data/shared","mountPath":"/mnt/data"}]`,
},
})
```

## Remote Proxy

When `CUBE_PROXY_NODE_IP` is set, data-plane requests connect directly to that IP and port while preserving the virtual sandbox host:

```text
URL: <CUBE_PROXY_SCHEME>://49999-<sandboxID>.<CUBE_SANDBOX_DOMAIN>/<envd-endpoint>
TCP: <CUBE_PROXY_NODE_IP>:<CUBE_PROXY_PORT_HTTP>
Host: 49999-<sandboxID>.<CUBE_SANDBOX_DOMAIN>
```

You can also set it directly:

```go
cfg := cubesandbox.Config{
APIURL: "http://10.0.0.1:3000",
TemplateID: "tpl-xxxxxxxx",
ProxyNodeIP: "10.0.0.1",
ProxyPortHTTP: 80,
ProxyScheme: "http",
SandboxDomain: "cube.app",
}
client := cubesandbox.NewClient(cfg)
```

## Integration Tests

Unit tests do not require a live service:

```bash
go test ./...
```

Live integration tests are behind the `integration` build tag. They require `CUBE_API_URL`, auto-discover a READY template from `/templates` when `CUBE_TEMPLATE_ID` is unset, and use `CUBE_PROXY_NODE_IP` for remote data-plane proxying when needed.

```bash
export CUBE_API_URL=http://<your-cubeapi-host>:3000
export CUBE_TEMPLATE_ID=<your-template-id>
export CUBE_PROXY_NODE_IP=<your-cubeproxy-node-ip>
export CUBE_PROXY_PORT_HTTP=80
export CUBE_PROXY_SCHEME=http
export CUBE_SANDBOX_DOMAIN=cube.app
go test -tags=integration -run Integration -count=1 ./...
```
204 changes: 204 additions & 0 deletions sdk/go/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright (c) 2026 Tencent Inc.
// SPDX-License-Identifier: Apache-2.0

package cubesandbox

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)

type ClientOption func(*Client)

// WithHTTPClient injects an HTTP client for SDK requests. It is primarily
// useful in tests or when the caller owns transport configuration.
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
if httpClient == nil {
return
}
c.controlHTTP = httpClient
c.dataHTTP = httpClient
}
}

type Client struct {
config Config
controlHTTP *http.Client
dataHTTP *http.Client
}

func NewClient(config Config, opts ...ClientOption) *Client {
config = normalizeConfig(config)
client := &Client{
config: config,
controlHTTP: newControlHTTPClient(config),
dataHTTP: newDataHTTPClient(config),
}
for _, opt := range opts {
opt(client)
}
return client
}

func (c *Client) Create(ctx context.Context, opts CreateOptions) (*Sandbox, error) {
payload, err := c.createPayload(opts)
if err != nil {
return nil, err
}

var sandbox Sandbox
if err := c.doJSON(ctx, http.MethodPost, "/sandboxes", payload, &sandbox, http.StatusOK, http.StatusCreated); err != nil {
return nil, err
}
c.attachSandbox(&sandbox)
return &sandbox, nil
}

func (c *Client) Connect(ctx context.Context, sandboxID string) (*Sandbox, error) {
payload := map[string]any{"timeout": durationSeconds(c.config.Timeout)}
var sandbox Sandbox
if err := c.doJSON(ctx, http.MethodPost, "/sandboxes/"+url.PathEscape(sandboxID)+"/connect", payload, &sandbox, http.StatusOK); err != nil {
return nil, err
}
c.attachSandbox(&sandbox)
return &sandbox, nil
}

func (c *Client) List(ctx context.Context) ([]SandboxInfo, error) {
var sandboxes []SandboxInfo
if err := c.doJSON(ctx, http.MethodGet, "/sandboxes", nil, &sandboxes, http.StatusOK); err != nil {
return nil, err
}
return sandboxes, nil
}

func (c *Client) ListV2(ctx context.Context) ([]SandboxInfo, error) {
var sandboxes []SandboxInfo
if err := c.doJSON(ctx, http.MethodGet, "/v2/sandboxes", nil, &sandboxes, http.StatusOK); err != nil {
return nil, err
}
return sandboxes, nil
}

func (c *Client) Health(ctx context.Context) (map[string]any, error) {
var health map[string]any
if err := c.doJSON(ctx, http.MethodGet, "/health", nil, &health, http.StatusOK); err != nil {
return nil, err
}
return health, nil
}

func (c *Client) createPayload(opts CreateOptions) (map[string]any, error) {
templateID := opts.TemplateID
if templateID == "" {
templateID = c.config.TemplateID
}
if templateID == "" {
return nil, fmt.Errorf("template is required. Set CUBE_TEMPLATE_ID or pass TemplateID")
}

timeout := opts.Timeout
if timeout <= 0 {
timeout = c.config.Timeout
}
payload := map[string]any{
"templateID": templateID,
"timeout": durationSeconds(timeout),
}
if len(opts.EnvVars) > 0 {
payload["envVars"] = opts.EnvVars
}
if len(opts.Metadata) > 0 {
payload["metadata"] = opts.Metadata
}
if opts.AllowInternetAccess != nil && !*opts.AllowInternetAccess {
payload["allowInternetAccess"] = false
}

network := map[string]any{}
if len(opts.Network.AllowOut) > 0 {
network["allowOut"] = opts.Network.AllowOut
}
if len(opts.Network.DenyOut) > 0 {
network["denyOut"] = opts.Network.DenyOut
}
if len(network) > 0 {
payload["network"] = network
}

for key, value := range opts.Extra {
payload[key] = value
}

return payload, nil
}

func (c *Client) attachSandbox(sandbox *Sandbox) {
sandbox.client = c
if sandbox.Domain == "" {
sandbox.Domain = c.config.SandboxDomain
}
}

func (c *Client) doJSON(ctx context.Context, method, path string, body any, out any, okStatuses ...int) error {
req, err := c.newRequest(ctx, method, path, body)
if err != nil {
return err
}

resp, err := c.controlHTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if !statusOK(resp.StatusCode, okStatuses) {
return apiErrorFromResponse(resp)
}
if out == nil || resp.StatusCode == http.StatusNoContent {
io.Copy(io.Discard, resp.Body)
return nil
}
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return err
}
return nil
}

func (c *Client) newRequest(ctx context.Context, method, path string, body any) (*http.Request, error) {
var reader io.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return nil, err
}
reader = bytes.NewReader(raw)
}

req, err := http.NewRequestWithContext(ctx, method, c.config.APIURL+path, reader)
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.config.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
}
return req, nil
}

func statusOK(statusCode int, okStatuses []int) bool {
for _, ok := range okStatuses {
if statusCode == ok {
return true
}
}
return false
}
Loading