Skip to main content

Image

Struct Image 

Source
pub struct Image { /* private fields */ }

Implementations§

Source§

impl Image

Source

pub fn is_sentry(&self) -> bool

True iff this image is backed by the no-virt sentry backend (vs KVM/HVF). Lets a caller (e.g. the CLI’s snapshot cache) tell which backend a loaded snapshot was baked for, so a cache hit can be matched to the requested backend.

Source

pub fn is_runsc(&self) -> bool

True iff this image is backed by the experimental runsc no-KVM backend.

Source§

impl Image

Source

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.json and restore.snap (typical: ~/.local/supermachine-snapshots/<name>/).
  • The restore.snap file itself; we read metadata.json from 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)
Source

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())?;
Source

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.

Source

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.

Source

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.

Source

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()?;
Source

pub fn snapshot_path(&self) -> &Path

Path to the snapshot file backing this image.

Source

pub fn memory_mib(&self) -> u32

Memory the snapshot was baked with. Vm::start uses this if VmConfig::with_memory_mib isn’t set.

Source

pub fn vcpus(&self) -> u32

vCPUs the snapshot was baked with.

Source

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.

Source

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.

Source

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/WorkingDirsh) 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).

Source

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).

Source

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.

Source

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.

Source

pub fn bake_runsc_with_config( image_ref: &str, dest_dir: impl Into<PathBuf>, config: SentryBakeConfig, ) -> Result<Image, Error>

Source

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-workload script (env merged OVER the image Env) AND the recorded metadata cmd/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 Vm over 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.
Source

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.

Source

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.

Source

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.

Source

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:

  • memorycgroup-v2 memory.max = config.with_memory_mib if set, else the image’s baked memory_mib (MiB → bytes). Over-budget guest trees are OOM-killed; the host + neighbors are protected.
  • CPUcgroup-v2 cpu.max = vcpus * 100ms per 100ms (config.with_vcpus if set, else the image’s baked vcpus), i.e. that many cores’ worth of CPU.
  • pidscgroup-v2 pids.max = config.with_sentry_pids_max if set, else SENTRY_DEFAULT_PIDS_MAX (fork-bomb protection).
  • uid-drop — the baked image User (numeric uid/uid:gid) drops the guest cell to that unprivileged host uid/gid before sealing.
  • env — the baked image Env seeds every cell (the per-exec env overlays it).
  • egress — the recorded egress_policy is 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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.)

Source

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.

Source

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.

Source

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()?;

Trait Implementations§

Source§

impl Clone for Image

Source§

fn clone(&self) -> Image

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Image

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl !Freeze for Image

§

impl RefUnwindSafe for Image

§

impl Send for Image

§

impl Sync for Image

§

impl Unpin for Image

§

impl UnsafeUnpin for Image

§

impl UnwindSafe for Image

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more