pub struct Image { /* private fields */ }Implementations§
Source§impl Image
impl Image
Source§impl Image
impl Image
Sourcepub fn from_snapshot(path: impl Into<PathBuf>) -> Result<Self, Error>
pub fn from_snapshot(path: impl Into<PathBuf>) -> Result<Self, Error>
Load an image from the on-disk artifacts produced by
supermachine run IMAGE. The argument can be either:
- The directory containing
metadata.jsonandrestore.snap(typical:~/.local/supermachine-snapshots/<name>/). - The
restore.snapfile itself; we readmetadata.jsonfrom its parent dir.
supermachine run nginx:1.27-alpine --detach && supermachine run --stop
# snapshot dir: ~/.local/supermachine-snapshots/nginx_1_27-alpine/On disk, that directory contains:
metadata.json # layers, memory, vcpus, etc.
restore.snap # captured VM state (CoW-mappable)
delta.squashfs # writable overlay layer (optional)Sourcepub fn from_oci(image_ref: &str) -> Result<Self, Error>
pub fn from_oci(image_ref: &str) -> Result<Self, Error>
Pull and bake an image from a registry reference, returning
the loadable Image. Equivalent to running
supermachine run <image_ref> --no-detach from a Rust app,
minus the daemon — you get the Image back, then call
Vm::start yourself.
Uses PullPolicy::Missing (cache-first) by default. For
other policies, see Image::from_oci_with_policy.
let image = Image::from_oci("nginx:1.27-alpine")?;
let vm = Vm::start(&image, &VmConfig::new())?;Sourcepub fn from_oci_with_policy(
image_ref: &str,
policy: PullPolicy,
) -> Result<Self, Error>
pub fn from_oci_with_policy( image_ref: &str, policy: PullPolicy, ) -> Result<Self, Error>
As Image::from_oci but with an explicit PullPolicy.
See PullPolicy for the cache + registry interaction
table.
Sourcepub fn from_oci_to_dir(
image_ref: &str,
policy: PullPolicy,
snapshots_dir: &Path,
name: Option<&str>,
) -> Result<Self, Error>
pub fn from_oci_to_dir( image_ref: &str, policy: PullPolicy, snapshots_dir: &Path, name: Option<&str>, ) -> Result<Self, Error>
Most explicit constructor: pull/bake into a specific snapshots directory, with an optional explicit name. Lets you keep multiple “supermachine snapshot stores” (e.g. per-project), or pin a snapshot under a name that differs from the image-derived default.
Sourcepub fn builder(image_ref: impl Into<String>) -> OciImageBuilder
pub fn builder(image_ref: impl Into<String>) -> OciImageBuilder
Builder for configurable bakes — env vars, cmd override, custom memory / port, custom snapshot name.
let image = Image::builder("nginx:1.27-alpine")
.with_name("nginx-prod")
.with_memory_mib(512)
.with_env("FOO", "bar")
.with_cmd(["nginx", "-g", "daemon off;"])
.build()?;The builder produces a different snapshot for each configuration — bake-time inputs are part of the snapshot fingerprint. Reuse a name across configurations and the previous snapshot is invalidated; pick distinct names if you need both side-by-side.
Sourcepub fn ensure_baked<F>(
name: impl Into<String>,
image_ref: impl Into<String>,
configure: F,
) -> Result<Image, Error>
pub fn ensure_baked<F>( name: impl Into<String>, image_ref: impl Into<String>, configure: F, ) -> Result<Image, Error>
Get an Image for name, baking it from image_ref
only if a compatible snapshot doesn’t already exist.
This is the right call for app startup. The first run
bakes (one-time cost: the registry pull + snapshot build,
e.g. ~12 s for rust:1-slim); subsequent runs see the
cached snapshot and return in microseconds. After a
cargo update that bumped the supermachine version, the
cached snapshot’s bake-key no longer matches the current
worker binary, and ensure_baked rebakes automatically —
no shell scripts, no manual rm -rf snapshots/.
configure is a builder closure: chain
OciImageBuilder methods like with_memory_mib,
with_cmd, with_env to customize the bake. Pass
|b| b for defaults.
use std::time::Duration;
use supermachine::{Image, VmConfig};
// Bake once on first run, reuse forever after — including
// across supermachine version upgrades.
let image = Image::ensure_baked("rust_1_slim", "rust:1-slim", |b| {
b.with_memory_mib(2048)
})?;
// Configure pool: 5 always-warm, scale to 50 under burst.
let pool = image.pool().min(5).max(50).build()?;
// Per-task path:
let vm = pool.acquire()?;
vm.write_file("/tmp/main.rs", b"fn main() { println!(\"hi\"); }")?;
let out = vm.exec_builder()
.argv(["sh", "-c", "rustc /tmp/main.rs -o /tmp/m && /tmp/m"])
.timeout(Duration::from_secs(30))
.output()?;Sourcepub fn snapshot_path(&self) -> &Path
pub fn snapshot_path(&self) -> &Path
Path to the snapshot file backing this image.
Sourcepub fn memory_mib(&self) -> u32
pub fn memory_mib(&self) -> u32
Memory the snapshot was baked with. Vm::start uses
this if VmConfig::with_memory_mib isn’t set.
Sourcepub fn start(&self, config: &VmConfig) -> Result<Vm, Error>
pub fn start(&self, config: &VmConfig) -> Result<Vm, Error>
Start a one-shot microVM from this image. Equivalent to
Vm::start(self, config) but reads more
naturally at the call site:
let image = Image::from_snapshot("path/to/snapshot")?;
let vm = image.start(&VmConfig::new())?;
// ... use vm ...
vm.stop()?;Use Image::acquire instead if you want a PooledVm
that returns to a (hidden) pool on Drop for cheaper
reuse — typical for evaluation harnesses, CI verifiers,
or any code that runs many short-lived VMs of the same
image back-to-back.
Sourcepub fn bake_kvm(
layer_tars: &[PathBuf],
kernel: &Path,
agent_initrd: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_kvm( layer_tars: &[PathBuf], kernel: &Path, agent_initrd: &Path, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Bake a KVM-bootable Image from OCI layer tarballs (Linux/x86_64).
Merges layer_tars (bottom-up, OCI whiteouts applied) into one rootfs
squashfs attached as /dev/vda, and writes a "backend":"kvm" snapshot
dir under dest_dir referencing kernel (bzImage) and agent_initrd
(the PID-1 exec agent). The result loads + runs via Image::start /
Image::acquire.
Remaining KVM-bake pieces (not yet automated here): OCI-ref → layer tars
(registry pull / docker save extraction) and a self-contained agent
initramfs. This consumes already-materialized layer tars + a prebuilt
agent initramfs.
Sourcepub fn bake_kvm_with_workload(
layer_tars: &[PathBuf],
kernel: &Path,
agent_initrd: &Path,
workload_script: Option<&str>,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_kvm_with_workload( layer_tars: &[PathBuf], kernel: &Path, agent_initrd: &Path, workload_script: Option<&str>, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Like bake_kvm but also bakes the OCI workload-launch
script (Entrypoint/Cmd/Env/WorkingDir → sh) into the rootfs at
/.supermachine/run-workload, so the guest starts the image’s command
(nginx, a server, …) and not just the agent. bake_kvm_from_ref derives
the script from the pulled image config; bake_kvm passes None (the
agent-only path, for prebuilt layer tars with no resolvable config).
Sourcepub fn bake_kvm_from_ref(
image_ref: &str,
kernel: &Path,
agent_initrd: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_kvm_from_ref( image_ref: &str, kernel: &Path, agent_initrd: &Path, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Bake a KVM-bootable Image directly from an OCI image reference
(Linux/x86_64) — e.g. "alpine:3.20" or "docker.io/library/busybox".
Pulls the image (registry-direct by default — no Docker daemon; honors
SUPERMACHINE_IMAGE_SOURCE + oci-layout: / oci-archive: refs),
resolves its amd64 layers, and delegates to Image::bake_kvm. Still
takes a prebuilt agent_initrd (the self-contained agent cpio is the
remaining bake piece).
Sourcepub fn bake_sentry_from_ref(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_sentry_from_ref( image_ref: &str, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Bake a Image for the sentry backend directly from an OCI image
reference (Linux/x86_64) — the no-virt path, for hosts where
[Vm::kvm_usable] is false (nested guests, CI, customer sandboxes).
Pulls the image (same registry-direct puller as [bake_kvm_from_ref]),
merges its amd64 layers into an extracted rootfs DIRECTORY under
dest_dir/rootfs (no kernel, no squashfs — the sentry serves files from
the dir), bakes the OCI workload-launch script into it, and writes a
"backend":"sentry" metadata.json. Run the result with
Image::run_sentry.
This is the no-config entry point (the image’s own Entrypoint/Cmd/Env);
the OciImageBuilder routes through Self::bake_sentry_with_config
to apply with_cmd / with_env / with_extra_file / with_mount /
with_volume / with_warmup.
Sourcepub fn bake_runsc_from_ref(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_runsc_from_ref( image_ref: &str, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Bake a Image for the experimental runsc backend directly from an
OCI image reference (Linux/x86_64). This intentionally shares the OCI
extraction/rootfs materialization path with sentry, but publishes
"backend":"runsc" metadata so the runtime dispatch can be implemented
independently around gVisor/runsc checkpoint/restore.
pub fn bake_runsc_with_config( image_ref: &str, dest_dir: impl Into<PathBuf>, config: SentryBakeConfig, ) -> Result<Image, Error>
Sourcepub fn bake_sentry_with_config(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
config: SentryBakeConfig,
) -> Result<Image, Error>
pub fn bake_sentry_with_config( image_ref: &str, dest_dir: impl Into<PathBuf>, config: SentryBakeConfig, ) -> Result<Image, Error>
Bake a sentry image applying an OciImageBuilder’s run-config
(Linux/x86_64). The config-aware counterpart of
Self::bake_sentry_from_ref — the builder’s cmd / env /
working_dir / user / extra_files / mounts / volumes / warmup
are mapped into the baked rootfs + metadata, mirroring what the KVM/HVF
bakes do for their backends:
- cmd/env/working_dir override the
/.supermachine/run-workloadscript (env merged OVER the image Env) AND the recorded metadatacmd/entrypoint/image_env/working_dir/user. - extra_files are written into the rootfs at bake time, confined
(their guest paths are joined under the rootfs,
../absolute escapes rejected). - mounts/volumes are persisted in
metadata.json(the sentry has no KVM-volume tail; this is how the mapping survives a reload). - warmup is run after the rootfs is built, against a transient
sentry
Vmover the freshly-baked image — its filesystem writes land IN-PLACE in the rootfs (the artifact), so the warm state is captured without an extra snapshot.
Sourcepub fn run_sentry(&self) -> Result<i32, Error>
pub fn run_sentry(&self) -> Result<i32, Error>
Run this sentry image’s baked workload to completion, confined to its rootfs, and return the workload’s exit code (Linux/x86_64).
This is the no-virt counterpart of cold-booting a KVM image: it launches
the image’s Entrypoint/Cmd (via the rootfs’s /bin/sh running the baked
/.supermachine/run-workload script, which exports the image Env and
cds into WorkingDir) under [crate::sentry::run]. Blocks until the
workload exits — the sentry has no long-lived exec channel yet (live
exec/expose_tcp into a running sentry workload is a later milestone),
so this is the current run surface, not Vm::start.
Errors if the image isn’t a sentry image. If the image declares no
command (has_workload == false), returns Ok(0) — nothing to launch.
Sourcepub fn run_sentry_classified(&self) -> Result<(i32, Option<SentryError>), Error>
pub fn run_sentry_classified(&self) -> Result<(i32, Option<SentryError>), Error>
As run_sentry, but also returns the structured
SentryError reason when the workload ended
other than a plain exit — so a caller can tell a sandbox-policy kill
(e.g. a forbidden-syscall SeccompViolation, which would otherwise just be
an ambiguous exit 159) apart from the workload’s own non-zero status. The
integer code is identical to run_sentry.
Sourcepub fn sentry_sandbox(&self) -> Result<Sandbox, Error>
pub fn sentry_sandbox(&self) -> Result<Sandbox, Error>
Open a handle-based Sandbox on this sentry
image’s rootfs (Linux/x86_64). Unlike run_sentry
(which runs the workload to completion), a Sandbox lets the image’s
workload run in the background (start_workload)
while you exec additional confined
commands against the same rootfs — the no-virt analogue of docker exec
(filesystem-shared; PID/net-namespace sharing is the later M5 work).
Errors if the image isn’t a sentry image.
This is the no-config entry point: it builds the sandbox with the image’s
own baked defaults (memory/vCPU-derived caps, a default pids.max, the
baked User uid-drop, the baked Env, the recorded egress policy). To
override the caps from a VmConfig (e.g. a smaller memory cap, a
different pids cap, or an uncapped run), use
sentry_sandbox_with.
Sourcepub fn sentry_sandbox_with(&self, config: &VmConfig) -> Result<Sandbox, Error>
pub fn sentry_sandbox_with(&self, config: &VmConfig) -> Result<Sandbox, Error>
Build a fully-configured Sandbox on this sentry
image’s rootfs, applying BOTH the image’s baked metadata AND config
(Linux/x86_64). This is the ONE place that builds + configures the sandbox
for the whole sentry surface (Vm::start’s sentry path, the pool, the
handle API), so a single tenant’s guest is host-safe by default:
- memory —
cgroup-v2 memory.max=config.with_memory_mibif set, else the image’s bakedmemory_mib(MiB → bytes). Over-budget guest trees are OOM-killed; the host + neighbors are protected. - CPU —
cgroup-v2 cpu.max=vcpus * 100msper100ms(config.with_vcpusif set, else the image’s bakedvcpus), i.e. that many cores’ worth of CPU. - pids —
cgroup-v2 pids.max=config.with_sentry_pids_maxif set, elseSENTRY_DEFAULT_PIDS_MAX(fork-bomb protection). - uid-drop — the baked image
User(numericuid/uid:gid) drops the guest cell to that unprivileged host uid/gid before sealing. - env — the baked image
Envseeds every cell (the per-execenv overlays it). - egress — the recorded
egress_policyis enforced on every outbound TCP.
with_sentry_uncapped disables the three
cgroup caps (trusted single-tenant tooling only). Errors if the image isn’t
a sentry image.
Sourcepub fn sentry_pool(&self) -> Result<Pool, Error>
pub fn sentry_pool(&self) -> Result<Pool, Error>
Start a PERSISTENT-supervisor Pool on this sentry
image’s rootfs (Linux/x86_64): one long-lived supervisor (rootfs dirfd +
rings + servicers set up once) that forks a fresh cell per
exec and fork-from-warm
acquire instance, amortizing the per-exec
setup. The image’s recorded egress policy is enforced on every cell. To
configure mounts / cgroup limits / uid-drop, build the pool from a
configured Sandbox:
image.sentry_sandbox()?.with_memory_limit(..).pool(). Errors if the image
isn’t a sentry image.
Sourcepub fn bake_kvm_auto(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_kvm_auto( image_ref: &str, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Bake a KVM-bootable Image from an OCI reference with zero asset
paths (Linux/x86_64) — the fully hands-off entry point:
let img = supermachine::Image::bake_kvm_auto("alpine", "/tmp/alpine-vm")?;The guest kernel, busybox, and in-VM agent are all sourced from the
bundled supermachine-kernel crate (its x86_64 sub-crate, selected by
target_arch), so the caller never points at a kernel build or builds
an initramfs. Internally it extracts the bundled minimal module-free
bzImage, assembles the agent initramfs in-process from the bundled
busybox + agent (no kernel modules — vsock/squashfs/overlay are built
in), then delegates to bake_kvm_from_ref.
All artifacts land under dest_dir, so the snapshot is self-contained.
Sourcepub fn bake_kvm_from_squashfs(
squashfs: &Path,
kernel: &Path,
agent_initrd: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_kvm_from_squashfs( squashfs: &Path, kernel: &Path, agent_initrd: &Path, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Wrap an already-built read-only rootfs.squashfs as a KVM-bootable
Image — the same rootfs format bake_kvm produces,
but with no layer re-extraction. This is the re-import side of the
in-VM builder’s RAM-density loop: a built image is flattened to a shared
read-only squashfs by commit_squashfs,
then booted back with an EMPTY tmpfs overlay upper — so the build’s files
live in the squashfs (page-cache shared CoW across every booted VM) instead
of each VM’s private tmpfs RAM. The squashfs is placed at dest/rootfs.squashfs
(copied in only if it isn’t already there — commit_squashfs can write it
straight into dest, making this copy-free).
Sourcepub fn bake_kvm_from_squashfs_auto(
squashfs: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error>
pub fn bake_kvm_from_squashfs_auto( squashfs: &Path, dest_dir: impl Into<PathBuf>, ) -> Result<Image, Error>
Hands-off variant of bake_kvm_from_squashfs:
source the guest kernel + agent initramfs from the bundled crate assets
(like bake_kvm_auto), so the caller only supplies
the committed squashfs + a destination.
Sourcepub fn build_kvm_initramfs(
agent_bin: &Path,
busybox_bin: &Path,
ordered_modules: &[PathBuf],
out_cpio: &Path,
) -> Result<(), Error>
pub fn build_kvm_initramfs( agent_bin: &Path, busybox_bin: &Path, ordered_modules: &[PathBuf], out_cpio: &Path, ) -> Result<(), Error>
Assemble a self-contained KVM agent initramfs (Linux/x86_64) in-process,
for use as the agent_initrd of bake_kvm /
bake_kvm_from_ref.
Builds a newc cpio whose PID-1 init mounts /proc + /dev, insmods
ordered_modules (.ko or .ko.zst, in order), mounts the rootfs disk
(/dev/vda) at /mnt, and execs the agent — no external cpio/busybox
build step. (A vsock-built-in kernel would drop the module list.)
Sourcepub fn acquire(&self) -> Result<PooledVm<'_>, Error>
pub fn acquire(&self) -> Result<PooledVm<'_>, Error>
Acquire a microVM from this image’s hidden pool. Returns
a PooledVm which Derefs to Vm and returns to
the pool on Drop. Use this for the common
“spin up a VM, do one task, throw it away, do another”
loop — the pool keeps re-restoring from the same snapshot
behind the scenes so per-iteration cost stays at the
snapshot-restore floor (~5 ms on Apple Silicon).
let image = Image::from_snapshot("path/to/rust-slim")?;
for src in ["fn main() {}", "fn main() { panic!() }"] {
let vm = image.acquire()?;
vm.write_file("/tmp/main.rs", src.as_bytes())?;
let out = vm.exec_builder()
.argv(["sh", "-c", "rustc /tmp/main.rs -o /tmp/m && /tmp/m"])
.timeout(Duration::from_secs(30))
.output()?;
println!("status={:?} out={:?}", out.status.code(), out.stdout);
// vm dropped here — returned to pool, restored from snapshot
}§Pool sizing
Image::acquire uses an ambient pool with default policy
(min=0, max=64, idle_timeout=60s,
acquire_timeout=60s). For an explicit policy use
Image::pool to build a Pool and call
pool.acquire() instead.
Per-acquire cost is the snapshot restore (~3 ms on Apple
Silicon) when an idle worker is available; cold spawn
(lazy-grow path) is ~15 ms. The pool auto-grows up to
max under burst and auto-evicts above-min workers
after they sit idle for idle_timeout.
Sourcepub fn acquire_with(&self, config: &VmConfig) -> Result<PooledVm<'_>, Error>
pub fn acquire_with(&self, config: &VmConfig) -> Result<PooledVm<'_>, Error>
Linux/KVM Image::acquire_with: acquires from an ambient warm pool built
lazily on first use (boot → snapshot once), so each acquire is a CoW
restore (~ms) instead of a cold boot (~1s). The pool is cached on the
Image (shared across clones); the first call’s config fixes its policy/
memory — for per-acquire control use an explicit Image::pool. Falls
back to a cold Vm::start if warming fails.
Sourcepub fn pool(&self) -> PoolBuilder<'_>
pub fn pool(&self) -> PoolBuilder<'_>
Configure an explicit pool against this image. Use when
you want auto-scaling or fine control over min/max/idle/
acquire timeouts; for the simple case
image.acquire() already manages an ambient default-
policy pool for you.
let image = Image::ensure_baked("rust_warm", "rust:1-slim", |b| b)?;
// Auto-scale 5..=50, evict idle workers after 60s,
// fail acquire if pool stays at max for >10s:
let pool = image.pool()
.min(5)
.max(50)
.idle_timeout(Duration::from_secs(60))
.acquire_timeout(Duration::from_secs(10))
.build()?;
let vm = pool.acquire()?;