Template Builder and Testing
This document explains how the current template-facing build API maps onto the snapshot-first internals, how committed snapshots move through the repository/runtime boundary, and what the relevant tests validate.
1) Public Surface vs Internal Model
User-facing builder API
-
TemplateBuildSpec(src/template/build_spec.rs)- Describes a declarative build request.
- Builder helpers include:
from_existing_rootfs(path)from_overlaybd_configs(global_config_path, image_config_path)run(cmd)env(key, value)workdir(path)apt(packages)alias(alias)resources(cpu_count, memory_mib)
- The current builder implementation accepts overlaybd-backed fresh builds and
rebuilds from committed snapshots. Fresh ext4-rootfs builds are not wired
through
TemplateBuilder.
-
TemplateBuilder(src/template/builder.rs)- Preserves the external template semantics for build / rebuild flows, while
committed snapshot lifecycle operations live in
SnapshotManager. - Main methods:
new()with_local_store_root(path)build_and_publish(snapshot_manager, config).awaitrebuild_and_publish(snapshot_manager, config, snapshot_id, base_snapshot).await
- Preserves the external template semantics for build / rebuild flows, while
committed snapshot lifecycle operations live in
Snapshot-first internals
-
StoredSnapshot/RunnableSnapshot(src/snapshot/types/)StoredSnapshotis the durable committed manifest state.- It stores snapshot metadata plus logical rootfs / attached-drive layer descriptions and memory layers.
- Snapshot publication also persists
firecracker-manifest.jsonfor launch-time metadata such as rootfs/memory virtual size and attached-driveread_onlyflags. - It does not store fixed local artifact locations such as
mem_image.jsonor runtime-derivedrootfs/image.json. RunnableSnapshotis a node-local resolved view with concrete artifact paths and runtime-ready overlaybd image configs.
-
SnapshotRepository/SnapshotRuntimeResolver(src/snapshot/repository/interfaces.rs)SnapshotRepositoryowns committed durable state.SnapshotRuntimeResolverturns committed state into node-local runnable paths.
2) Build and Publish Flow
TemplateBuilder::build_and_publish(...) does the following:
- Prepare the build base from either:
- a committed snapshot (
rebuild_and_publish) - overlaybd configs (
from_overlaybd_configs)
- a committed snapshot (
- Start a temporary Firecracker sandbox with the requested CPU/memory.
- Execute template steps in order.
- Probe
envd, kernel, and firecracker versions from the running guest and executable. - Pause the sandbox and export a
FirecrackerSnapshotManifestcarrying metadata for repository publication - Publish those local artifacts into the configured snapshot repository.
The important boundary is:
- the builder API describes what to build
- local build artifacts are manager-owned temporary outputs
- committed snapshot records store logical durable state
- backend layout conventions determine where committed files and shared layers live
- runtime resolution turns committed snapshot state into node-local runnable paths
3) Repository Layout
The default backend is the POSIX filesystem backend. Given a backend root like
/path/to/store/repository:
- committed snapshot manifest:
/path/to/store/repository/snapshots/<id>/snapshot.json
- committed fixed-layout files:
/path/to/store/repository/snapshots/<id>/vm_state.bin/path/to/store/repository/snapshots/<id>/firecracker-manifest.json
- shared managed layers:
/path/to/store/repository/managed-layers/<digest>.overlaybd.commit
- alias bindings:
/path/to/store/repository/catalog/aliases/<alias>/path/to/store/repository/catalog/aliases/<alias>.lock
Alias locking is per-alias, not a single global aliases.lock file.
snapshot.json intentionally does not repeat local build-artifact paths such
as mem_image.json or rootfs/image.json. Runtime code derives node-local
image configs from:
- repository root
- snapshot id
- attached drive id
- managed layer digest
firecracker-manifest.json complements this by persisting launch-time virtual
size and attached-drive mode metadata without embedding node-local runtime
paths.
4) Load and Launch Flow
Typical runtime usage is:
SnapshotManager::load_committed(id_or_alias).awaitSnapshotManager::resolve_runnable(stored).awaitFirecrackerSandbox::from_snapshot(&runnable, &SandboxLaunchConfig::default())sandbox.start().await
If you want the two snapshot-manager calls combined, use
SnapshotManager::load_runnable(id_or_alias).await.
resolve_runnable(...) is where backend-neutral committed state becomes
node-local launch inputs:
memory_layers-> runtimememory/image.jsonrootfs.layers-> runtimerootfs/image.jsonattached_drives[].layers-> runtimedrives/<id>/image.jsonattached_drives[].read_only-> runtime mount mode fordrives/<id>firecracker-manifest.json-> runtime manifest hydrated with node-local artifact paths- committed
vm_state.bin-> runnable vm-state path
5) Minimal Example
#![allow(unused)]
fn main() {
use agentenv::cfg::ConfigManager;
use agentenv::image::ImageResolver;
use agentenv::sandbox::{FirecrackerSandbox, SandboxExecutor, SandboxLaunchConfig};
use agentenv::snapshot::SnapshotManager;
use agentenv::template::{TemplateBuildSpec, TemplateBuilder};
let builder = TemplateBuilder::new();
let snapshot_manager = SnapshotManager::new()?;
let config = ConfigManager::global()?.config();
let image_resolver = ImageResolver::new(config);
let image_config = image_resolver
.resolve(image_resolver.default_image())
.await?
.overlaybd_config_path;
let alias = "my-template-v1";
builder
.build_and_publish(
&snapshot_manager,
TemplateBuildSpec::new()
.from_overlaybd_config(image_config)
.alias(alias)
.resources(1, 128)
.run("mkdir -p /workspace")
.workdir("/workspace")
.env("MARK", "ready")
.run("printf '%s' \"$MARK\" > mark.txt"),
)
.await?;
let runnable = snapshot_manager
.load_runnable(alias)
.await?
.expect("template should exist");
let mut sandbox =
FirecrackerSandbox::from_snapshot(&runnable, &SandboxLaunchConfig::default())?;
sandbox.start().await?;
let out = sandbox.run_command("cat", &["/workspace/mark.txt"]).await?;
assert_eq!(out.exit_code, 0);
assert_eq!(out.stdout.trim(), "ready");
sandbox.stop().await?;
}
6) State Transition
The template-facing state transition is:
- local build artifacts
- committed
snapshot.json+firecracker-manifest.json+ fixed files + managed layers - node-local runtime-derived image configs
- Firecracker resume inputs
7) What the Tests Validate
tests/integration/snapshot.rs covers:
- building and publishing from overlaybd configs
- loading committed snapshots by alias / id through the snapshot manager
- resolving committed snapshots into runnable snapshots
- launching Firecracker sandboxes from resolved snapshots
- rebuilding from committed snapshots while preserving base state
- listing and deleting committed snapshots through the template API surface
tests/integration/snapshot_attached_drive.rs covers overlaybd attached-drive handling
across sandbox capture, publish, resolve, launch, and rebuild flows, including:
- committed metadata preserving
readOnly - runtime mount mode for readonly and writable drives
- rebuild keeping attached-drive metadata stable
tests/snapshot_oss_e2e_test.rs covers OSS-backed snapshot publication,
resolution, alias cleanup, missing managed-layer failure modes, and deletion.
Repository/backend unit tests under src/snapshot/repository/backends/ cover:
- artifact import
- alias handling
- committed snapshot deletion
- runtime cache behavior
8) Common Failure Areas
- overlaybd base config paths are missing or invalid
ublkis disabled while using overlaybd build bases- Linux host prerequisites for Firecracker / KVM / namespaces are missing
- repository alias conflicts during publish
- runtime resolver cannot materialize local paths from committed artifacts