-
Notifications
You must be signed in to change notification settings - Fork 3
feat: snapshot and resume example #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,127 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env -S npm run tsn -T | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: Devbox Snapshot and Resume | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slug: devbox-snapshot-resume | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use_case: Create a devbox, snapshot its disk, resume from the snapshot, and demonstrate that changes in the original devbox do not affect the clone. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Create a devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Write a file to the devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Create a disk snapshot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Create a new devbox from the snapshot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Modify the file on the original devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Verify the clone has the original content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Shutdown both devboxes and delete the snapshot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tags: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - snapshot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - resume | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - cleanup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prerequisites: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - RUNLOOP_API_KEY | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: yarn tsn -T examples/devbox-snapshot-resume.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test: yarn test:examples | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { RunloopSDK } from '@runloop/api-client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { wrapRecipe, runAsCli } from './_harness'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { RecipeContext, RecipeOutput } from './types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const FILE_PATH = '/home/user/welcome.txt'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ORIGINAL_CONTENT = 'hello world!'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MODIFIED_CONTENT = 'original devbox has changed the welcome message'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function recipe(ctx: RecipeContext): Promise<RecipeOutput> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { cleanup } = ctx; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sdk = new RunloopSDK({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bearerToken: process.env['RUNLOOP_API_KEY'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create a devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dbxOriginal = await sdk.devbox.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'dbx_original', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| launch_parameters: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resource_size_request: 'X_SMALL', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keep_alive_time_seconds: 60 * 5, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup.add(`devbox:${dbxOriginal.id}`, () => sdk.devbox.fromId(dbxOriginal.id).shutdown()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Write a file to the original devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await dbxOriginal.file.write(FILE_PATH, ORIGINAL_CONTENT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Read and display the file contents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const catOriginalBefore = await dbxOriginal.cmd.exec(`cat ${FILE_PATH}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const originalContentBefore = await catOriginalBefore.stdout(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create a disk snapshot of the original devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const snapshot = await dbxOriginal.snapshotDisk({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'my-snapshot', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup.add(`snapshot:${snapshot.id}`, () => snapshot.delete()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create a new devbox from the snapshot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dbxClone = await sdk.devbox.createFromSnapshot(snapshot.id, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'dbx_clone', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| launch_parameters: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resource_size_request: 'X_SMALL', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| keep_alive_time_seconds: 60 * 5, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup.add(`devbox:${dbxClone.id}`, () => sdk.devbox.fromId(dbxClone.id).shutdown()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. .addResource(x) we know which one it is with just the id xxx_yyy |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Modify the file on the original devbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await dbxOriginal.file.write(FILE_PATH, MODIFIED_CONTENT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await dbxOriginal.file.write(FILE_PATH, ORIGINAL_CONTENT); | |
| // Read and display the file contents | |
| const catOriginalBefore = await dbxOriginal.cmd.exec(`cat ${FILE_PATH}`); | |
| const originalContentBefore = await catOriginalBefore.stdout(); | |
| // Create a disk snapshot of the original devbox | |
| const snapshot = await dbxOriginal.snapshotDisk({ | |
| name: 'my-snapshot', | |
| }); | |
| cleanup.add(`snapshot:${snapshot.id}`, () => snapshot.delete()); | |
| // Create a new devbox from the snapshot | |
| const dbxClone = await sdk.devbox.createFromSnapshot(snapshot.id, { | |
| name: 'dbx_clone', | |
| launch_parameters: { | |
| resource_size_request: 'X_SMALL', | |
| keep_alive_time_seconds: 60 * 5, | |
| }, | |
| }); | |
| cleanup.add(`devbox:${dbxClone.id}`, () => sdk.devbox.fromId(dbxClone.id).shutdown()); | |
| // Modify the file on the original devbox | |
| await dbxOriginal.file.write(FILE_PATH, MODIFIED_CONTENT); | |
| await dbxOriginal.file.write({ file_path: FILE_PATH, contents: ORIGINAL_CONTENT }); | |
| await dbxOriginal.file.write({ file_path: FILE_PATH, contents: MODIFIED_CONTENT }); |
Steps of Reproduction ✅
1. From the repo root `/workspace/api-client-ts`, run the documented example command `yarn
tsn -T examples/devbox-snapshot-resume.ts` (as specified in the front‑matter of
`examples/devbox-snapshot-resume.ts:23`).
2. Execution enters `recipe()` in `examples/devbox-snapshot-resume.ts:36-41`, constructs a
`RunloopSDK` instance, and creates a devbox via `sdk.devbox.create(...)` at lines 43-50,
yielding `dbxOriginal`.
3. The script calls `await dbxOriginal.file.write(FILE_PATH, ORIGINAL_CONTENT);` at
`examples/devbox-snapshot-resume.ts:54`, passing two positional arguments (string path and
string contents). At runtime this invokes `DevboxFileOps.write(params, options?)` defined
in `src/sdk/devbox.ts:469-485`, where `params` is expected to be a
`DevboxWriteFileContentsParams` object but actually receives the string `FILE_PATH`, and
`options` receives the string `ORIGINAL_CONTENT`.
4. Inside `DevboxFileOps.write`, the call
`this.client.devboxes.writeFileContents(this.devboxId, params, options)` forwards these
incorrect arguments to the generated API client. The API contract is validated by tests in
`tests/api-resources/devboxes/devboxes.test.ts:577-595`, which show that
`writeFileContents` requires `client.devboxes.writeFileContents('id', { contents:
'contents', file_path: 'file_path' })`. Because the example passes a bare string instead
of the required params object, the HTTP request body is malformed, causing the write to
fail so `/home/user/welcome.txt` is never created and the example flow (subsequent `cat`
and checks at lines 57-84) fails when run.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** examples/devbox-snapshot-resume.ts
**Line:** 54:77
**Comment:**
*Type Error: The calls to the devbox file write API are using the wrong argument shape (`path, contents` positional arguments) instead of the expected single params object, so at runtime the SDK will send an invalid request body to `writeFileContents`, likely causing the file not to be written and subsequent `cat` commands and checks to fail.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weird for an example maybe we lift this out of the example itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catExecResult or something