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 run [OPTIONS] <IMAGE> [-- COMMAND...]
|
||||||
|
ecrou provision [OPTIONS] <IMAGE> [-- COMMAND...]
|
||||||
ecrou probe
|
ecrou probe
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -283,6 +284,17 @@ ecrou probe
|
|||||||
| `--no-cache` | false | Fresh download, ignore the cache |
|
| `--no-cache` | false | Fresh download, ignore the cache |
|
||||||
| `-v, --verbose` | false | Diagnostics; raises the libkrun log level |
|
| `-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
|
`ecrou probe` reports what a run would find: KVM working (open, ioctl
|
||||||
version, nested-virt state), `libkrun.so.1` loadable, the cache
|
version, nested-virt state), `libkrun.so.1` loadable, the cache
|
||||||
directory writable. It is the confinement diagnostic and the support
|
directory writable. It is the confinement diagnostic and the support
|
||||||
|
|||||||
@@ -21,6 +21,23 @@ pub struct Cli {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
/// Run a command in a fresh microVM from an image
|
/// Run a command in a fresh microVM from an image
|
||||||
Run(RunArgs),
|
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
|
/// Report what a run would find on this host
|
||||||
Probe,
|
Probe,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,69 @@ fn main() -> std::process::ExitCode {
|
|||||||
logger::init(cli.verbose);
|
logger::init(cli.verbose);
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Run(args) => cmd_run(&args, cli.verbose),
|
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(),
|
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 {
|
fn cmd_run(args: &RunArgs, cli_verbose: bool) -> std::process::ExitCode {
|
||||||
let mut options = RunOptions {
|
let mut options = RunOptions {
|
||||||
image: args.image.clone(),
|
image: args.image.clone(),
|
||||||
|
|||||||
@@ -129,6 +129,19 @@ impl RootfsCache {
|
|||||||
format!("Failed to create cache directory {}", self.dir.display())
|
format!("Failed to create cache directory {}", self.dir.display())
|
||||||
})?;
|
})?;
|
||||||
download_image(&source, &cache_path, &arch, progress)?;
|
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 {
|
} else {
|
||||||
log::debug!("Using cached tarball: {}", cache_path.display());
|
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.
|
/// Run one machine to completion and return the payload's exit code.
|
||||||
pub fn run(options: &RunOptions, progress: &dyn Progress) -> Result<i32> {
|
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() {
|
if options.image.is_empty() {
|
||||||
return Err(Error::Other(anyhow!("no image given")));
|
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 ram_mib = parse_memory_mib(&options.memory).map_err(Error::Other)?;
|
||||||
let vcpus = vcpus();
|
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() {
|
let hostname = if options.hostname.is_empty() {
|
||||||
format!("ecrou-vm-{}", utils::random_suffix(HOSTNAME_SUFFIX_LEN))
|
format!("ecrou-vm-{}", utils::random_suffix(HOSTNAME_SUFFIX_LEN))
|
||||||
} else {
|
} else {
|
||||||
@@ -129,7 +136,7 @@ pub fn run(options: &RunOptions, progress: &dyn Progress) -> Result<i32> {
|
|||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(Error::Other)?;
|
.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
|
let shares = share_pairs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, gs)| gs.clone())
|
.map(|(_, gs)| gs.clone())
|
||||||
|
|||||||
Reference in new issue
Block a user