Skip to content
Closed
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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,4 @@ dist
# bin
bin

/examples
.beamignore
.beamignore
81 changes: 62 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,43 +38,86 @@ yarn add @beamcloud/beam-js@rc

## Quickstart

Run a simple Node.js server in a sandbox.
Create a sandbox, run code, write a file, and expose a small HTTP server.

```typescript
import { beamOpts, Image, Sandbox } from "@beamcloud/beam-js";

beamOpts.token = process.env.BEAM_TOKEN!;
beamOpts.workspaceId = process.env.BEAM_WORKSPACE_ID!;

async function main() {
const image = new Image({
baseImage: "node:20",
commands: [
"apt update",
"apt install -y nodejs npm",
"git clone https://github.com/beam-cloud/quickstart-node.git /app",
],
});

const sandbox = new Sandbox({
name: "quickstart",
image: image,
cpu: 2,
memory: 1024,
image: Image.fromRegistry("python:3.11-slim"),
cpu: 1,
memory: "512Mi",
keepWarmSeconds: 300,
});

const instance = await sandbox.create();

const process4 = await instance.exec(["sh", "-c", "cd /app && node server.js"]);

const url = await instance.exposePort(3000);
console.log(`Server is running at ${url}`);
try {
const result = await instance.runCode("print('hello from Beam JS')");
console.log(result);

await instance.fs.writeText("/workspace/index.html", "hello from a sandbox");
await instance.exec(
[
"python3",
"-u",
"-m",
"http.server",
"8765",
"--bind",
"0.0.0.0",
],
{ cwd: "/workspace" },
);

const url = await instance.exposePort(8765);
console.log(`Server is running at ${url}`);
} finally {
await instance.terminate();
}
}

main();
```

## Sandbox examples

Run these from a checkout of this repository:

```bash
BEAM_TOKEN=... npx tsx examples/sandbox-basic.ts
BEAM_TOKEN=... npx tsx examples/sandbox-http.ts
BEAM_TOKEN=... npx tsx examples/sandbox-snapshot.ts
```

Docker-in-Docker support requires an image with Docker installed and
`dockerEnabled: true`:

```bash
BEAM_TOKEN=... npx tsx examples/sandbox-docker.ts
```

The SDK defaults to Beam production (`https://app.beam.cloud`). Set
`BEAM_GATEWAY_URL` only when testing a development gateway.

## Production sandbox e2e tests

The normal test suite does not create cloud resources. Run the sandbox e2e tests
explicitly:

```bash
BEAM_TOKEN=... npm run test:e2e:sandbox
```

Docker coverage is opt-in because it builds a Docker-enabled image:

```bash
BEAM_TOKEN=... BEAM_SANDBOX_E2E_DOCKER=1 npm run test:e2e:sandbox
```

## Support

- [Documentation](https://docs.beam.cloud/v2/reference/ts-sdk)
Expand Down
35 changes: 35 additions & 0 deletions examples/sandbox-basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { beamOpts, Image, Sandbox } from "../lib";

beamOpts.token = process.env.BEAM_TOKEN || "";
beamOpts.gatewayUrl = process.env.BEAM_GATEWAY_URL || "https://app.beam.cloud";

async function main() {
const sandbox = new Sandbox({
name: "js-sandbox-basic",
image: Image.fromRegistry("python:3.11-slim"),
cpu: 1,
memory: "512Mi",
keepWarmSeconds: 300,
});

const instance = await sandbox.create();
try {
const result = await instance.runCode("print('hello from Beam JS')");
console.log(result);

await instance.fs.writeText("/workspace/message.txt", "hello filesystem");
console.log(await instance.fs.readText("/workspace/message.txt"));

const process = await instance.exec("printf streamed-log");
for await (const line of process.logs) {
console.log(line);
}
} finally {
await instance.terminate();
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
34 changes: 34 additions & 0 deletions examples/sandbox-docker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { beamOpts, Image, Sandbox } from "../lib";

beamOpts.token = process.env.BEAM_TOKEN || "";
beamOpts.gatewayUrl = process.env.BEAM_GATEWAY_URL || "https://app.beam.cloud";

async function main() {
const image = new Image({ pythonVersion: "python3.11" }).withDocker();
const sandbox = new Sandbox({
name: "js-sandbox-docker",
image,
cpu: 2,
memory: "2Gi",
keepWarmSeconds: 300,
dockerEnabled: true,
});

const instance = await sandbox.create();
try {
const version = await instance.docker.version();
await version.wait();
console.log(await version.stdout.read());

const hello = await instance.docker.run("hello-world", ["--rm"]);
await hello.wait();
console.log(await hello.stdout.read());
} finally {
await instance.terminate();
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
45 changes: 45 additions & 0 deletions examples/sandbox-http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { beamOpts, Image, Sandbox } from "../lib";

beamOpts.token = process.env.BEAM_TOKEN || "";
beamOpts.gatewayUrl = process.env.BEAM_GATEWAY_URL || "https://app.beam.cloud";

async function main() {
const sandbox = new Sandbox({
name: "js-sandbox-http",
image: Image.fromRegistry("python:3.11-slim"),
cpu: 1,
memory: "512Mi",
keepWarmSeconds: 300,
});

const instance = await sandbox.create();
try {
await instance.fs.writeText("/workspace/index.html", "hello from a sandbox");
await instance.exec(
[
"python3",
"-u",
"-m",
"http.server",
"8765",
"--bind",
"0.0.0.0",
],
{ cwd: "/workspace" },
);

const url = await instance.exposePort(8765);
console.log(url);
const response = await fetch(url);
console.log(await response.text());
} catch (error) {
throw error;
} finally {
await instance.terminate();
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
40 changes: 40 additions & 0 deletions examples/sandbox-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { beamOpts, Image, Sandbox } from "../lib";

beamOpts.token = process.env.BEAM_TOKEN || "";
beamOpts.gatewayUrl = process.env.BEAM_GATEWAY_URL || "https://app.beam.cloud";

async function main() {
const sandbox = new Sandbox({
name: "js-sandbox-snapshot",
image: Image.fromRegistry("python:3.11-slim"),
cpu: 1,
memory: "512Mi",
keepWarmSeconds: 300,
});

const instance = await sandbox.create();
let restored;
try {
await instance.exec([
"python3",
"-u",
"-c",
"from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler; ThreadingHTTPServer(('0.0.0.0', 8899), SimpleHTTPRequestHandler).serve_forever()",
]);

const checkpointId = await instance.snapshot();
restored = await Sandbox.createFromSnapshot(checkpointId);
const url = await restored.exposePort(8899);
console.log(url);
} finally {
if (restored) {
await restored.terminate();
}
await instance.terminate();
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
6 changes: 2 additions & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const config: Config = {
// moduleNameMapper: {},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: ["<rootDir>/dist"],

// Activates notifications for test results
// notify: false,
Expand Down Expand Up @@ -163,9 +163,7 @@ const config: Config = {
// ],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
testPathIgnorePatterns: ["/node_modules/", "<rootDir>/dist/"],

// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
Expand Down
6 changes: 2 additions & 4 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { camelCaseToSnakeCaseKeys } from "./util";

export interface BeamClientOpts {
token: string;
workspaceId: string;
workspaceId?: string;
gatewayUrl?: string;
timeout?: number;
}
Expand All @@ -25,9 +25,6 @@ class BeamClient {
if (!beamOpts.gatewayUrl) {
throw new Error("Beam gateway URL is not set");
}
if (!beamOpts.workspaceId) {
throw new Error("Beam workspace ID is not set");
}

if (!this._client) {
this._client = axios.create({
Expand Down Expand Up @@ -79,6 +76,7 @@ export {
Sandbox,
SandboxInstance,
SandboxFileSystem,
SandboxDockerManager,
} from "./resources/abstraction/sandbox";

// Export Image classes and types
Expand Down
28 changes: 28 additions & 0 deletions lib/resources/abstraction/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class Image {
this.config.pythonPackages = this._sanitizePythonPackages(pythonPackages);

this.config.commands = commands;
this.config.buildSteps = buildSteps;
this.config.baseImage = baseImage;
this.config.baseImageCreds = this._processCredentials(baseImageCreds);
this.config.envVars = envVars;
Expand Down Expand Up @@ -298,6 +299,7 @@ export class Image {
gpu: this.config.gpu,
ignorePython: this.config.ignorePython,
imageId: this.config.imageId,
buildSteps: this.config.buildSteps,
};

const response = await this.verifyImageBuild(request);
Expand Down Expand Up @@ -355,6 +357,7 @@ export class Image {
secrets: this.config.secrets,
gpu: this.config.gpu,
ignorePython: this.config.ignorePython,
buildSteps: this.config.buildSteps,
};

let lastResponse: BuildImageResponse = { success: false };
Expand Down Expand Up @@ -553,6 +556,31 @@ export class Image {
return this;
}

/**
* Install Docker Engine, Docker CLI, Compose, and Buildx in the image.
*
* Use this with `new Sandbox({ image, dockerEnabled: true })`.
*/
withDocker(): Image {
const dockerInstallCommands = [
"apt-get update && apt-get install -y ca-certificates curl gnupg lsb-release",
"mkdir -p /etc/apt/keyrings && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg",
'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null',
"apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
"ln -sf /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose",
"docker --version && docker compose version && docker-compose version",
"apt-get clean && rm -rf /var/lib/apt/lists/*",
];

this.config.buildSteps.push(
...dockerInstallCommands.map((command) => ({
command,
type: "shell" as const,
})),
);
return this;
}

/**
* Sync files using FileSyncer
*/
Expand Down
Loading
Loading