mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
feat(cli): ecrou provision bakes a command into a cache entry
prepare_provisioned and persist were sitting there with no consumer: the machinery for "install once, every later boot carries it" that one-shot machines cannot do on their own - packages, users, anything short of a share - and no way to reach it. ecrou provision IMAGE -- COMMAND is the missing surface. It boots a machine over a freshly prepared rootfs, runs the command, and persists the tree into the cache entry, marked provisioned; a marked entry is extracted as-is on every later call, the command skipped. A failing command leaves the entry unprovisioned for the next attempt, and --no-cache re-downloads before re-provisioning. The engine behind run splits out as vm::run_prepared: the boot half of run, taking the rootfs the provisioner is about to persist. run itself is prepare plus that. One cache fix the provision flow surfaced: a digest refresh replaced the tarball but left the old .provisioned marker, so a fresh pristine image would present as provisioned and skip provisioning for good. The marker now dies with the entry it belonged to.
This commit is contained in:
1 parent
1fb3f0c076
commit
d9f9c79dc4
5 files changed
+117
-9
No files matched your search
@@ -269,6 +269,7 @@ composable. A boot failure leaves the files absent.
|
||||
|
||||
```
|
||||
ecrou run [OPTIONS] <IMAGE> [-- COMMAND...]
|
||||
ecrou provision [OPTIONS] <IMAGE> [-- COMMAND...]
|
||||
ecrou probe
|
||||
```
|
||||
|
||||
@@ -283,6 +284,17 @@ ecrou probe
|
||||
| `--no-cache` | false | Fresh download, ignore the cache |
|
||||
| `-v, --verbose` | false | Diagnostics; raises the libkrun log level |
|
||||
|
||||
`ecrou provision IMAGE -- COMMAND` bakes a command into the image's
|
||||
cache entry: the command runs in a machine over a freshly prepared
|
||||
rootfs and the result is persisted, so every later boot of that image
|
||||
carries what the command installed — packages, users, whatever a
|
||||
one-shot boot cannot keep. A marked entry is reused as-is (the
|
||||
command is skipped), `--no-cache` re-downloads and re-provisions, and
|
||||
a failing command leaves the entry unprovisioned for the next attempt.
|
||||
The marker dies with the entry: a digest refresh drops it, so a moved
|
||||
upstream image is provisioned again, never silently served pristine
|
||||
under an old marker.
|
||||
|
||||
`ecrou probe` reports what a run would find: KVM working (open, ioctl
|
||||
version, nested-virt state), `libkrun.so.1` loadable, the cache
|
||||
directory writable. It is the confinement diagnostic and the support
|
||||
|
||||
@@ -21,6 +21,23 @@ pub struct Cli {
|
||||
pub enum Command {
|
||||
/// Run a command in a fresh microVM from an image
|
||||
Run(RunArgs),
|
||||
/// Bake a command into an image's cache entry: the command runs in
|
||||
/// a machine over a freshly prepared rootfs, and the result is
|
||||
/// persisted, so every later boot of the image carries what the
|
||||
/// 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
|
||||
#[arg(long)]
|
||||
no_cache: bool,
|
||||
/// The provisioning command. An argument vector after `--` is
|
||||
/// taken literally; a single quoted line runs through the
|
||||
/// image's shell.
|
||||
#[arg(last = true)]
|
||||
command: Vec<String>,
|
||||
},
|
||||
/// Report what a run would find on this host
|
||||
Probe,
|
||||
}
|
||||
|
||||
@@ -18,10 +18,69 @@ fn main() -> std::process::ExitCode {
|
||||
logger::init(cli.verbose);
|
||||
match cli.command {
|
||||
Command::Run(args) => cmd_run(&args, cli.verbose),
|
||||
Command::Provision {
|
||||
image,
|
||||
no_cache,
|
||||
command,
|
||||
} => cmd_provision(&image, no_cache, &command, cli.verbose),
|
||||
Command::Probe => cmd_probe(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_provision(
|
||||
image: &str,
|
||||
no_cache: bool,
|
||||
command: &[String],
|
||||
cli_verbose: bool,
|
||||
) -> std::process::ExitCode {
|
||||
use ecrou::rootfs::{PrepareRequest, RootfsCache};
|
||||
|
||||
if command.is_empty() {
|
||||
return fail(&anyhow::anyhow!(
|
||||
"provision needs a command: ecrou provision <IMAGE> -- <COMMAND...>"
|
||||
));
|
||||
}
|
||||
let level = if cli_verbose {
|
||||
LogLevel::Verbose
|
||||
} else {
|
||||
LogLevel::Normal
|
||||
};
|
||||
let mut options = RunOptions {
|
||||
image: image.to_owned(),
|
||||
command: command.to_owned(),
|
||||
log_level: level,
|
||||
// The machine exists to change the rootfs, not to cross a
|
||||
// share: the workspace would ride the persist into the cache.
|
||||
..RunOptions::default()
|
||||
};
|
||||
options.workdir = "/".to_owned();
|
||||
|
||||
let progress: &dyn Progress = &TermProgress::new();
|
||||
let request = PrepareRequest {
|
||||
image: image.to_owned(),
|
||||
arch: String::new(),
|
||||
no_cache,
|
||||
};
|
||||
let dir = match RootfsCache::default_dir() {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => return fail(&anyhow::Error::new(err)),
|
||||
};
|
||||
let cache = RootfsCache::new(dir);
|
||||
let outcome = cache.prepare_provisioned(&request, progress, |rootfs| {
|
||||
let code = ecrou::vm::run_prepared(rootfs, &options)?;
|
||||
if code != 0 {
|
||||
anyhow::bail!(
|
||||
"the provisioning command exited {code}; the cache entry was left unprovisioned"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
match outcome {
|
||||
Ok(_rootfs) => std::process::ExitCode::SUCCESS,
|
||||
Err(err) => fail(&anyhow::Error::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_run(args: &RunArgs, cli_verbose: bool) -> std::process::ExitCode {
|
||||
let mut options = RunOptions {
|
||||
image: args.image.clone(),
|
||||
|
||||
@@ -129,6 +129,19 @@ impl RootfsCache {
|
||||
format!("Failed to create cache directory {}", self.dir.display())
|
||||
})?;
|
||||
download_image(&source, &cache_path, &arch, progress)?;
|
||||
// Whatever was provisioned belonged to the entry this just
|
||||
// replaced. A stale marker here would present the pristine
|
||||
// image as provisioned and skip the next provisioning for
|
||||
// good.
|
||||
let marker = sidecar(&cache_path, PROVISIONED_SUFFIX);
|
||||
if marker.exists() {
|
||||
std::fs::remove_file(&marker).with_context(|| {
|
||||
format!(
|
||||
"Failed to drop the stale provisioned marker {}",
|
||||
marker.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
log::debug!("Using cached tarball: {}", cache_path.display());
|
||||
}
|
||||
|
||||
+16
-9
@@ -91,6 +91,21 @@ impl Default for RunOptions {
|
||||
|
||||
/// Run one machine to completion and return the payload's exit code.
|
||||
pub fn run(options: &RunOptions, progress: &dyn Progress) -> Result<i32> {
|
||||
let mut request = PrepareRequest::new(options.image.clone());
|
||||
request.arch = options.arch.clone();
|
||||
request.no_cache = options.no_cache;
|
||||
let cache = RootfsCache::new(RootfsCache::default_dir()?);
|
||||
let rootfs = cache
|
||||
.prepare(&request, progress)
|
||||
.context("preparing the rootfs")?;
|
||||
run_prepared(&rootfs, options)
|
||||
}
|
||||
|
||||
/// Boot one machine over an already-prepared rootfs. This is the whole
|
||||
/// boot half of [`run`], and the engine the provisioner drives: it
|
||||
/// wants a machine over the extracted tree it is about to persist,
|
||||
/// before that tree becomes the cache entry every later boot sees.
|
||||
pub fn run_prepared(rootfs: &PreparedRootfs, options: &RunOptions) -> Result<i32> {
|
||||
if options.image.is_empty() {
|
||||
return Err(Error::Other(anyhow!("no image given")));
|
||||
}
|
||||
@@ -101,14 +116,6 @@ pub fn run(options: &RunOptions, progress: &dyn Progress) -> Result<i32> {
|
||||
let ram_mib = parse_memory_mib(&options.memory).map_err(Error::Other)?;
|
||||
let vcpus = vcpus();
|
||||
|
||||
let mut request = PrepareRequest::new(options.image.clone());
|
||||
request.arch = options.arch.clone();
|
||||
request.no_cache = options.no_cache;
|
||||
let cache = RootfsCache::new(RootfsCache::default_dir()?);
|
||||
let rootfs = cache
|
||||
.prepare(&request, progress)
|
||||
.context("preparing the rootfs")?;
|
||||
|
||||
let hostname = if options.hostname.is_empty() {
|
||||
format!("ecrou-vm-{}", utils::random_suffix(HOSTNAME_SUFFIX_LEN))
|
||||
} else {
|
||||
@@ -129,7 +136,7 @@ pub fn run(options: &RunOptions, progress: &dyn Progress) -> Result<i32> {
|
||||
})
|
||||
.transpose()
|
||||
.map_err(Error::Other)?;
|
||||
let share_pairs = collect_shares(&rootfs, options, capture_share).map_err(Error::Other)?;
|
||||
let share_pairs = collect_shares(rootfs, options, capture_share).map_err(Error::Other)?;
|
||||
let shares = share_pairs
|
||||
.iter()
|
||||
.map(|(_, gs)| gs.clone())
|
||||
|
||||
Reference in new issue
Block a user