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
6 changes: 6 additions & 0 deletions .changeset/famous-pugs-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@vercel/sandbox": minor
"sandbox": minor
---

Add support for drives via a new `Drive` class and CLI commands.
21 changes: 21 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"ai-example": "0.1.1",
"charts-python-example": "0.1.4",
"dev-server-example": "0.1.4",
"sandbox-filesystem-snapshots-example": "0.0.17",
"install-packages-example": "0.1.4",
"private-repo-example": "0.1.4",
"sandbox-basics-example": "0.1.4",
"workflow-code-runner-example": "0.1.6",
"@vercel/pty-tunnel": "2.0.3",
"@vercel/pty-tunnel-server": "0.0.2",
"sandbox": "3.0.0",
"@vercel/sandbox": "2.0.0"
},
"changesets": [
"famous-pugs-scream"
]
}
11 changes: 11 additions & 0 deletions packages/sandbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# sandbox

## 3.2.0-beta.0

### Minor Changes

- Add support for drives via a new `Drive` class and CLI commands. ([#196](https://github.com/vercel/sandbox/pull/196))

### Patch Changes

- Updated dependencies [[`d51a1b7cd19de15018dab2140cbc08ba646c9e2f`](https://github.com/vercel/sandbox/commit/d51a1b7cd19de15018dab2140cbc08ba646c9e2f)]:
- @vercel/sandbox@2.2.0-beta.0

## 3.1.0

### Minor Changes
Expand Down
21 changes: 20 additions & 1 deletion packages/sandbox/docs/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## `sandbox --help`

```
sandbox 3.1.0
sandbox 3.2.0-beta.0

▲ sandbox [options] <command>

Expand All @@ -22,6 +22,7 @@ Commands:
snapshot <name> Take a snapshot of the filesystem of a sandbox
snapshots Manage sandbox snapshots
sessions Manage sandbox sessions
drives Manage sandbox drives
login Log in to the Sandbox CLI
logout Log out of the Sandbox CLI

Expand Down Expand Up @@ -89,6 +90,7 @@ Options:
--snapshot, -s <snapshot_id> Start the sandbox from a snapshot ID [optional]
--env <key=value>, -e=<key=value> Environment variables to set for the command
--tag <key=value>, -t=<key=value> Key-value tags to associate with the sandbox (e.g. --tag env=staging)
--mount <drive:path[:mode]> Attach a drive to the sandbox. Format: "drive:/path[:read-only|read-write]".
--snapshot-expiration <DURATION|none> Default snapshot expiration. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--keep-last-snapshots <COUNT> Keep only the N most recent snapshots of this sandbox (1-10). [optional]
--keep-last-snapshots-for <DURATION|none> Expiration applied to kept snapshots. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
Expand Down Expand Up @@ -146,6 +148,7 @@ Options:
--snapshot, -s <snapshot_id> Start the sandbox from a snapshot ID [optional]
--env <key=value>, -e=<key=value> Default environment variables for sandbox commands
--tag <key=value>, -t=<key=value> Key-value tags to associate with the sandbox (e.g. --tag env=staging)
--mount <drive:path[:mode]> Attach a drive to the sandbox. Format: "drive:/path[:read-only|read-write]".
--snapshot-expiration <DURATION|none> Default snapshot expiration. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--keep-last-snapshots <COUNT> Keep only the N most recent snapshots of this sandbox (1-10). [optional]
--keep-last-snapshots-for <DURATION|none> Expiration applied to kept snapshots. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
Expand Down Expand Up @@ -400,6 +403,22 @@ Commands:
rm | delete <snapshot_id> [...snapshot_id] Delete one or more snapshots.
```

## `sandbox drives`

```
sandbox drives

▲ sandbox drives [options] <command>

For command help, run `sandbox drives <command> --help`

Commands:

ls | list List drives for the specified account and project.
get-or-create <name> Create a drive if it does not already exist, or retrieve it.
rm | delete <name> [...name] Delete one or more drives.
```

## `sandbox config`

```
Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sandbox",
"description": "Command line interface for Vercel Sandbox",
"version": "3.1.0",
"version": "3.2.0-beta.0",
"scripts": {
"clean": "rm -rf node_modules dist",
"sandbox": "ts-node ./src/sandbox.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/sandbox/scripts/print-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const docs = {
"sandbox connect": "connect --help",
"sandbox snapshot": "snapshot --help",
"sandbox snapshots": "snapshots --help",
"sandbox drives": "drives --help",
"sandbox config": "config --help",
"sandbox login": "login --help",
"sandbox logout": "logout --help",
Expand Down
2 changes: 2 additions & 0 deletions packages/sandbox/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { snapshot } from "./commands/snapshot";
import { snapshots } from "./commands/snapshots";
import { sessions } from "./commands/sessions";
import { config } from "./commands/config";
import { drives } from "./commands/drives";

export const app = (opts?: { withoutAuth?: boolean; appName?: string }) =>
subcommands({
Expand All @@ -35,6 +36,7 @@ export const app = (opts?: { withoutAuth?: boolean; appName?: string }) =>
snapshot,
snapshots,
sessions,
drives,
...(!opts?.withoutAuth && {
login,
logout,
Expand Down
92 changes: 92 additions & 0 deletions packages/sandbox/src/args/drive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as cmd from "cmd-ts";
import chalk from "chalk";
import type { SandboxMountMode, SandboxMounts } from "@vercel/sandbox";

export interface DriveMount {
drive: string;
path: string;
mode?: SandboxMountMode;
}

export type DriveMounts = SandboxMounts;

export const driveName = cmd.extendType(cmd.string, {
displayName: "name",
description: "The name of the drive",
async from(input) {
const value = input.trim();
if (value.length === 0) {
throw new Error("Drive name cannot be empty.");
}
return value;
},
});

export const driveMount = cmd.extendType(cmd.string, {
displayName: "drive:path[:mode]",
description:
'Drive mount in the format "drive:/path[:read-only|read-write]".',
async from(input) {
return parseDriveMount(input);
},
});

export const driveMounts = cmd.extendType(cmd.array(driveMount), {
async from(input): Promise<DriveMounts> {
const mounts: DriveMounts = Object.create(null);

for (const mount of input) {
mounts[mount.path] = { drive: mount.drive, mode: mount.mode };
}

return mounts;
},
});

export const mounts = cmd.multioption({
long: "mount",
type: driveMounts,
description:
'Attach a drive to the sandbox. Format: "drive:/path[:read-only|read-write]".',
});

export const driveMaxSize = cmd.extendType(cmd.number, {
displayName: "BYTES",
async from(input) {
if (!Number.isInteger(input) || input < 1) {
throw new Error(
`Invalid max size: ${input}. Must be a positive integer number of bytes.`,
);
}
return input;
},
});

export function parseDriveMount(input: string): DriveMount {
const [drive, path, mode, ...rest] = input.split(":");
const validModes: SandboxMountMode[] = ["read-only", "read-write"];

if (rest.length > 0 || !drive || path === undefined) {
throw new Error(
[
`Invalid drive mount: ${input}.`,
`${chalk.bold("hint:")} Use "drive:/path" or "drive:/path:read-only".`,
].join("\n"),
);
}

if (mode !== undefined && !validModes.includes(mode as SandboxMountMode)) {
throw new Error(
[
`Invalid drive mount mode: ${mode}.`,
`${chalk.bold("hint:")} Valid modes are: ${validModes.join(", ")}`,
].join("\n"),
);
}

return {
drive,
path,
mode: mode as SandboxMountMode | undefined,
};
}
16 changes: 15 additions & 1 deletion packages/sandbox/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Sandbox, APIError, Snapshot } from "@vercel/sandbox";
import { Sandbox, APIError, Snapshot, Drive } from "@vercel/sandbox";
import { version } from "./pkg";
import chalk from "chalk";
import { tmpdir } from "node:os";
Expand Down Expand Up @@ -46,6 +46,20 @@ export const snapshotClient: Pick<
withErrorHandling(() => Snapshot.tree({ fetch: fetchWithUserAgent, ...params })),
};

export const driveClient: Pick<typeof Drive, "getOrCreate" | "list"> & {
delete(drive: Drive): Promise<void>;
} = {
getOrCreate: (params) =>
withErrorHandling(() =>
Drive.getOrCreate({ fetch: fetchWithUserAgent, ...params }),
),
list: (params) =>
withErrorHandling(() =>
Drive.list({ fetch: fetchWithUserAgent, ...params } as typeof params),
),
delete: (drive) => withErrorHandling(() => drive.delete()),
};

const fetchWithUserAgent: typeof globalThis.fetch = (input, init) => {
const headers = new Headers(
init?.headers ??
Expand Down
6 changes: 6 additions & 0 deletions packages/sandbox/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { buildNetworkPolicy } from "../util/network-policy";
import { ObjectFromKeyValue } from "../args/key-value-pair";
import { buildKeepLastSnapshotsPayload } from "../util/keep-last-snapshots";
import { printSandboxSummary } from "../util/print-sandbox-summary";
import { mounts } from "../args/drive";

export const args = {
name: cmd.option({
Expand Down Expand Up @@ -58,6 +59,7 @@ export const args = {
type: ObjectFromKeyValue,
description: "Key-value tags to associate with the sandbox (e.g. --tag env=staging)",
}),
mounts,
...snapshotRetentionArgs,
...networkPolicyArgs,
scope,
Expand Down Expand Up @@ -86,6 +88,7 @@ export const create = cmd.command({
connect,
envVars,
tags,
mounts,
snapshotExpiration,
keepLastSnapshots,
keepLastSnapshotsFor,
Expand All @@ -111,6 +114,7 @@ export const create = cmd.command({
const persistent = !nonPersistent;
const resources = vcpus ? { vcpus } : undefined;
const tagsObj = Object.keys(tags).length > 0 ? tags : undefined;
const mountsObj = Object.keys(mounts).length > 0 ? mounts : undefined;
const spinner = silent ? undefined : ora("Creating sandbox...").start();
const sandbox = snapshot
? await sandboxClient.create({
Expand All @@ -125,6 +129,7 @@ export const create = cmd.command({
networkPolicy,
env: envVars,
tags: tagsObj,
mounts: mountsObj,
persistent,
snapshotExpiration: snapshotExpiration ? ms(snapshotExpiration) : undefined,
keepLastSnapshots: keepLastSnapshotsPayload,
Expand All @@ -142,6 +147,7 @@ export const create = cmd.command({
networkPolicy,
env: envVars,
tags: tagsObj,
mounts: mountsObj,
persistent,
snapshotExpiration: snapshotExpiration ? ms(snapshotExpiration) : undefined,
keepLastSnapshots: keepLastSnapshotsPayload,
Expand Down
Loading
Loading