Skip to content

Commit 9e4b618

Browse files
committed
feat(snapshot): add backend-neutral cloud snapshots
Route snapshot lifecycle through backend-specific implementations and add cloud snapshot contracts and restore support. Preserve typed snapshot references across SDKs, add instance archive export parity, and update tests and documentation.
1 parent 6cee1f5 commit 9e4b618

76 files changed

Lines changed: 7479 additions & 4406 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/cli/lib/commands/self_cmd.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,11 +1104,12 @@ async fn run_downgrade_with_db(
11041104
&& ctx.operation.phase() < DowngradePhase::ArtifactsReverted
11051105
{
11061106
let spinner = ui::Spinner::start("Checking", "retained snapshot graph");
1107-
let result = microsandbox::snapshot::downgrade::preflight_managed_v066(
1108-
ctx.db.inner(),
1109-
ctx.snapshots_dir,
1110-
)
1111-
.await;
1107+
let result =
1108+
microsandbox::backend::local_snapshot_downgrade::preflight_managed_v066(
1109+
ctx.db.inner(),
1110+
ctx.snapshots_dir,
1111+
)
1112+
.await;
11121113
match result {
11131114
Ok(plan) => {
11141115
spinner.finish_success("Checked");
@@ -1175,7 +1176,7 @@ async fn run_downgrade_with_db(
11751176
.set_phase(DowngradePhase::ArtifactsReverting)?;
11761177
if let Some(plan) = snapshot_plan {
11771178
let spinner = ui::Spinner::start("Reverting", "snapshot artifacts");
1178-
match microsandbox::snapshot::downgrade::execute_managed_v066(
1179+
match microsandbox::backend::local_snapshot_downgrade::execute_managed_v066(
11791180
ctx.db.inner(),
11801181
ctx.operation.recovery_dir(),
11811182
plan,

crates/cli/lib/commands/snapshot.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! `msb snapshot` command — manage disk snapshots.
22
33
use clap::{Args, Subcommand};
4-
use microsandbox::Snapshot;
4+
use microsandbox::{Snapshot, SnapshotReference};
55

66
use crate::ui;
77

@@ -225,7 +225,7 @@ async fn create(args: SnapshotCreateArgs) -> anyhow::Result<()> {
225225
spinner.finish_success("Snapshotted");
226226
if !args.quiet {
227227
println!("{}", snap.digest());
228-
println!("{}", snap.path().display());
228+
println!("{}", format_reference(&snap.reference()));
229229
}
230230
Ok(())
231231
}
@@ -259,7 +259,8 @@ async fn list(args: SnapshotListArgs) -> anyhow::Result<()> {
259259
"migration_state": s.migration_state(),
260260
"migration_error_code": s.migration_error_code(),
261261
"created_at": ui::format_json_datetime(&s.created_at().and_utc()),
262-
"artifact_path": s.path().display().to_string(),
262+
"reference": format_reference(&s.reference()),
263+
"reference_kind": s.reference().kind(),
263264
})
264265
})
265266
.collect();
@@ -315,7 +316,7 @@ async fn inspect(args: SnapshotInspectArgs) -> anyhow::Result<()> {
315316
let m = snap.manifest();
316317

317318
ui::detail_kv("Digest", snap.digest());
318-
ui::detail_kv("Path", &snap.path().display().to_string());
319+
ui::detail_kv("Reference", &format_reference(&snap.reference()));
319320
ui::detail_kv("Image", &m.image.reference);
320321
ui::detail_kv("Image Manifest", &m.image.manifest_digest);
321322
ui::detail_kv("Scope", format_scope(m.scope));
@@ -427,7 +428,7 @@ async fn save(args: SnapshotSaveArgs) -> anyhow::Result<()> {
427428
async fn load(args: SnapshotLoadArgs) -> anyhow::Result<()> {
428429
let handle = Snapshot::load(&args.archive, args.dest.as_deref()).await?;
429430
println!("{}", handle.digest());
430-
println!("{}", handle.path().display());
431+
println!("{}", format_reference(&handle.reference()));
431432
Ok(())
432433
}
433434

@@ -449,6 +450,10 @@ fn format_scope(scope: microsandbox::SnapshotScope) -> &'static str {
449450
}
450451
}
451452

453+
fn format_reference(reference: &SnapshotReference) -> String {
454+
reference.value().to_string()
455+
}
456+
452457
fn format_size(bytes: u64) -> String {
453458
const KIB: u64 = 1024;
454459
const MIB: u64 = KIB * 1024;

docs/sandboxes/snapshots.mdx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@ description: Capture a sandbox's writable layer as a portable artifact
44
icon: "code-branch"
55
---
66

7-
<Tooltip tip="Disk snapshots are local-only. On microsandbox cloud, use a named volume for state that must survive sandbox replacement."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>
8-
9-
A snapshot is a portable, on-disk capture of a sandbox's writable filesystem. Move it with `scp`, archive it as `.tar.zst`, or boot fresh sandboxes from it.
7+
A snapshot captures a sandbox's writable filesystem. The local backend stores a
8+
portable on-disk artifact; microsandbox cloud stores it in managed object
9+
storage by default, or in a directory on the organization's host volume when a
10+
destination is supplied.
1011

1112
<Note>
1213
Snapshots are **disk-only** and require a sandbox that is not running. Stopped and crashed sandboxes can be snapshotted; running, draining, and paused sandboxes are rejected.
1314
</Note>
1415

16+
<Note>
17+
Snapshot objects now expose a backend-neutral `reference` (and reference kind)
18+
instead of a host `path` or storage `location`. When upgrading, pass the
19+
snapshot object directly where supported, or pass its reference to the restore
20+
API. This keeps the same application code valid for local and cloud backends.
21+
</Note>
22+
1523
## What gets captured
1624

1725
| Captured | Not captured |
@@ -41,20 +49,26 @@ msb run --name worker --from-snapshot after-pip-install \
4149
```
4250

4351
<Tip>
44-
By default, the snapshot lives at `~/.microsandbox/snapshots/after-pip-install/`. That whole directory is the snapshot.
52+
On the local backend, the snapshot lives at
53+
`~/.microsandbox/snapshots/after-pip-install/`. In cloud, omitting a destination
54+
creates a managed snapshot and returns its stable snapshot reference.
4555
</Tip>
4656

4757
## Snapshot a sandbox
4858

49-
Snapshot under a bare name, resolved to `~/.microsandbox/snapshots/<name>/` by default. The name is the snapshot's identity; pass a destination directory to create the artifact on a different volume (`DIR/<name>`). Either way the directory is the whole artifact; move it with save/load (or plain `mv`):
59+
Snapshot under a bare name. Locally, it resolves to
60+
`~/.microsandbox/snapshots/<name>/`. In cloud, it is uploaded to managed storage
61+
and recorded by the control plane. Passing a destination stores an artifact at
62+
`DIR/<name>` locally or at the corresponding path on the organization's host
63+
volume in cloud.
5064

5165
<CodeGroup>
5266
```rust Rust
5367
use microsandbox::Sandbox;
5468

5569
let h = Sandbox::get("baseline").await?;
5670

57-
// Resolves under ~/.microsandbox/snapshots/<name>/
71+
// Local: default store. Cloud: managed snapshot storage.
5872
let snap = h.snapshot("after-pip-install").await?;
5973

6074
println!("{}", snap.digest()); // sha256:...
@@ -65,7 +79,7 @@ import { Sandbox } from "microsandbox";
6579

6680
const h = await Sandbox.get("baseline");
6781

68-
// Resolves under ~/.microsandbox/snapshots/<name>/
82+
// Local: default store. Cloud: managed snapshot storage.
6983
const snap = await h.snapshot("after-pip-install");
7084

7185
console.log(snap.digest); // sha256:...
@@ -76,7 +90,7 @@ from microsandbox import Sandbox
7690

7791
h = await Sandbox.get("baseline")
7892

79-
# Resolves under ~/.microsandbox/snapshots/<name>/
93+
# Local: default store. Cloud: managed snapshot storage.
8094
snap = await h.snapshot("after-pip-install")
8195

8296
print(snap.digest) # sha256:...
@@ -88,7 +102,7 @@ if err != nil {
88102
return err
89103
}
90104

91-
// Resolves under ~/.microsandbox/snapshots/<name>/
105+
// Local: default store. Cloud: managed snapshot storage.
92106
snap, err := h.Snapshot(ctx, "after-pip-install")
93107

94108
fmt.Println(snap.Digest()) // sha256:...
@@ -147,7 +161,10 @@ msb run --name worker --from-snapshot after-pip-install -- python -V
147161

148162
</CodeGroup>
149163

150-
Booting validates the snapshot, resolves the pinned image, and gives the new sandbox its own writable copy.
164+
Booting validates the snapshot, resolves the pinned image, and gives the new
165+
sandbox its own writable copy. When you already have a `Snapshot` or
166+
`SnapshotHandle`, pass its stable reference rather than its local path. The
167+
Python, TypeScript, and Go accept the snapshot object directly.
151168

152169
## List, inspect, and remove
153170

@@ -213,9 +230,13 @@ msb snapshot reindex
213230

214231
</CodeGroup>
215232

216-
`list` and `get` use a local index for fast lookup. If the index gets out of sync, `reindex` rebuilds it from the snapshot artifacts on disk.
233+
`list` and `get` use the active backend. `reindex`, `list_dir`, `save`, `load`,
234+
direct artifact-file operations, and full payload verification keep the same
235+
SDK surface on every backend but currently return a typed `Unsupported` error
236+
in cloud. Snapshot path references themselves are supported in cloud and are
237+
resolved relative to the organization's host volume.
217238

218-
## Move snapshots between machines
239+
## Move local snapshots between machines
219240

220241
The snapshot directory is the whole artifact; there is no hidden daemon state. Copy the directory directly, or save it as an archive:
221242

docs/sdk/go/sandbox.mdx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,17 +1031,20 @@ Use a disk image as the root filesystem and optionally provide the inner filesys
10311031
<div className="msb-tags"><span className="msb-tag is-builder">option</span></div>
10321032

10331033
```go
1034-
func WithFromSnapshot(pathOrName string) SandboxOption
1034+
func WithFromSnapshot[T SnapshotSeed](snapshot T) SandboxOption
10351035
```
10361036

1037-
Boot from a snapshot artifact by bare name or filesystem path. Mutually exclusive with [`WithImage`](#withimage). See [Snapshots](/sdk/go/snapshots).
1037+
Boot from a snapshot using a backend-relative reference. Mutually exclusive
1038+
with [`WithImage`](#withimage). Pass a `SnapshotArtifact` or `SnapshotHandle`
1039+
directly to preserve its typed identifier-or-path reference without inspecting
1040+
the selected backend.
10381041

10391042
<p className="msb-label">Parameters</p>
10401043

10411044
<div className="msb-params">
10421045
<div className="msb-param">
1043-
<div className="msb-param-key"><code>pathOrName</code><span className="msb-type">string</span></div>
1044-
<div className="msb-param-desc">Snapshot artifact path or bare name.</div>
1046+
<div className="msb-param-key"><code>snapshot</code><span className="msb-type">string | *SnapshotArtifact | *SnapshotHandle</span></div>
1047+
<div className="msb-param-desc">Snapshot object or backend-relative compatibility string.</div>
10451048
</div>
10461049
</div>
10471050

0 commit comments

Comments
 (0)