feat(cli): provision layers from a spec file

A provision command baked by hand - `ecrou provision alpine -- apk
add ...` - cannot be reviewed or shared: the artifact worth keeping is
a diff in git, not a shell history line. A spec file is that diff.
`ecrou provision --spec image.yaml` parses base, packages and a run
block, derives the provisioning script from the base's own package
manager (apt, apk, dnf, discovered at provision time on the running
machine), and bakes the result into a cache entry keyed by the spec's
content hash: the same file never provisions twice, an edited file
never reuses the old layer, and the key hashes the raw bytes so the
layer is stable across ecrou versions.

`--layer NAME` names the entry, and `ecrou run --layer NAME` boots it
directly - no image resolution, no download; the layer is whatever
the provision call persisted under that name. A keyed entry has no
tarball of its own, so the base downloads only on the first
provisioning and every later run extracts the finished layer.
This commit is contained in:
vhaudiquet committed 2026-10-29 18:06:50 +01:00
1 parent dad5f61833
commit 878ce29b8d
7 files changed
+455 -31

No files matched your search

+22 -5
View File
@@ -27,9 +27,20 @@ pub enum Command {
/// command installed. Cached entries are reused as-is; --no-cache
/// re-downloads and re-provisions.
Provision {
/// Distribution name or OCI image reference
image: String,
/// Ignore the cache and download a fresh image
/// Distribution name or OCI image reference; the spec's `base`
/// when omitted and the spec carries one
image: Option<String>,
/// A provisioning spec file (YAML: base, packages, run) — the
/// layer it describes is keyed by the file's content, so the
/// same spec never provisions twice and a changed spec never
/// reuses an old layer
#[arg(long)]
spec: Option<std::path::PathBuf>,
/// Name the provisioned layer, so runs can boot it by name
/// (`run --layer NAME`) without re-deriving anything
#[arg(long)]
layer: Option<String>,
/// Ignore the cache: re-download the base and re-provision
#[arg(long)]
no_cache: bool,
/// The provisioning command. An argument vector after `--` is
@@ -44,8 +55,10 @@ pub enum Command {
#[derive(Args, Debug)]
pub struct RunArgs {
/// Distribution name or OCI image reference
pub image: String,
/// Distribution name or OCI image reference; not needed with
/// `--layer`, which boots a provisioned cache entry
#[arg(required_unless_present = "layer")]
pub image: Option<String>,
/// Command to run in the machine; the image's default shell when
/// omitted. An argument vector after `--` is taken literally; a
/// single quoted line runs through the image's shell.
@@ -73,6 +86,10 @@ pub struct RunArgs {
/// Ignore the cache and download a fresh image
#[arg(long)]
pub no_cache: bool,
/// Boot a named provisioned layer (see `provision --layer`)
/// instead of resolving an image
#[arg(long)]
pub layer: Option<String>,
}
/// Parse one `HOST:GUEST[:ro]` share specification. The guest path must