mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
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.
249 lines
7.6 KiB
Rust
249 lines
7.6 KiB
Rust
//! The ecrou CLI: flags onto the library API, exit codes out. All
|
|
//! terminal output lives here; the library stays silent behind `log`
|
|
//! and the progress sink.
|
|
|
|
mod cli;
|
|
mod logger;
|
|
mod progress;
|
|
|
|
use clap::Parser;
|
|
use ecrou::vm::RunOptions;
|
|
use ecrou::{LogLevel, NoProgress, Progress};
|
|
|
|
use cli::{parse_env, parse_share, Cli, Command, RunArgs};
|
|
use progress::TermProgress;
|
|
|
|
fn main() -> std::process::ExitCode {
|
|
let cli = Cli::parse();
|
|
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(),
|
|
command: args.command.clone(),
|
|
memory: args.memory.clone(),
|
|
workdir: args.workdir.clone(),
|
|
log_level: if args.quiet {
|
|
LogLevel::Quiet
|
|
} else if cli_verbose {
|
|
LogLevel::Verbose
|
|
} else {
|
|
LogLevel::Normal
|
|
},
|
|
no_cache: args.no_cache,
|
|
capture: args.capture.clone(),
|
|
..RunOptions::default()
|
|
};
|
|
for spec in &args.env {
|
|
match parse_env(spec) {
|
|
Ok((k, v)) => options.env.push((k, v)),
|
|
Err(err) => return fail(&err),
|
|
}
|
|
}
|
|
let mut shares = Vec::new();
|
|
for spec in &args.shares {
|
|
match parse_share(spec) {
|
|
Ok(share) => shares.push(share),
|
|
Err(err) => return fail(&err),
|
|
}
|
|
}
|
|
// The first read-write share is the workspace (its guest path rides
|
|
// into the guest as ECROU_WORKSPACE); everything else is a plain
|
|
// share.
|
|
if let Some(pos) = shares.iter().position(|s| !s.read_only) {
|
|
options.workspace = Some(shares.remove(pos));
|
|
}
|
|
options.shares = shares;
|
|
|
|
let progress: &dyn Progress = if args.quiet {
|
|
&NoProgress
|
|
} else {
|
|
&TermProgress::new()
|
|
};
|
|
match ecrou::vm::run(&options, progress) {
|
|
Ok(code) => std::process::ExitCode::from(code.clamp(0, 255) as u8),
|
|
Err(err) => fail(&anyhow::Error::new(err)),
|
|
}
|
|
}
|
|
|
|
fn cmd_probe() -> std::process::ExitCode {
|
|
use ecrou::krun;
|
|
|
|
let mut ok = true;
|
|
|
|
print!("kvm: ");
|
|
match krun::kvm_probe() {
|
|
Ok(()) => println!("ok"),
|
|
Err(err) => {
|
|
ok = false;
|
|
println!("unavailable\n {:#}", err);
|
|
}
|
|
}
|
|
|
|
print!("libkrun: ");
|
|
match krun::Krun::load() {
|
|
Ok(krun) => {
|
|
println!("ok");
|
|
print!("nested virt: ");
|
|
match krun.check_nested_virt() {
|
|
Ok(true) => println!("available"),
|
|
Ok(false) => println!("not available (only matters for nested runs)"),
|
|
Err(err) => println!("unknown ({err:#})"),
|
|
}
|
|
}
|
|
Err(err) => {
|
|
ok = false;
|
|
println!("not loadable\n {:#}", err);
|
|
}
|
|
}
|
|
|
|
print!("cache: ");
|
|
match ecrou::rootfs::RootfsCache::default_dir() {
|
|
Ok(dir) => {
|
|
// The run flow creates the cache on demand; probe mirrors it.
|
|
if let Err(err) = std::fs::create_dir_all(&dir) {
|
|
ok = false;
|
|
println!("{} not creatable ({})", dir.display(), err);
|
|
return probe_done(ok);
|
|
}
|
|
let probe = dir.join(".ecrou-probe");
|
|
match std::fs::write(&probe, b"") {
|
|
Ok(()) => {
|
|
let _ = std::fs::remove_file(&probe);
|
|
println!("{} (writable)", dir.display());
|
|
}
|
|
Err(err) => {
|
|
ok = false;
|
|
println!("{} not writable ({})", dir.display(), err);
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
ok = false;
|
|
println!("unavailable ({err:#})");
|
|
}
|
|
}
|
|
|
|
probe_done(ok)
|
|
}
|
|
|
|
fn probe_done(ok: bool) -> std::process::ExitCode {
|
|
if ok {
|
|
std::process::ExitCode::SUCCESS
|
|
} else {
|
|
std::process::ExitCode::FAILURE
|
|
}
|
|
}
|
|
|
|
fn fail(err: &anyhow::Error) -> std::process::ExitCode {
|
|
eprintln!("Error: {err:#}");
|
|
std::process::ExitCode::FAILURE
|
|
}
|
|
|
|
/// T6, the founding rule as a test: strace a full run and fail on any
|
|
/// denied syscall. Needs the built binary, libkrun, and strace, so it
|
|
/// runs in CI (`cargo test -- --ignored`).
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
#[ignore = "needs the ecrou binary, libkrun, and strace; runs in CI"]
|
|
fn t6_no_mount_or_namespace_syscalls() {
|
|
let denied = "mount,umount2,unshare,setns,pivot_root,open_tree,move_mount,fsopen,fsmount";
|
|
// CARGO_BIN_EXE_* exists only for integration tests, which the
|
|
// conventions forbid; CI points ECROU_BIN at the built binary.
|
|
let exe = std::env::var("ECROU_BIN")
|
|
.expect("T6 needs ECROU_BIN pointing at the built ecrou binary");
|
|
let trace_path = std::env::temp_dir().join("ecrou-t6.strace");
|
|
let out = std::process::Command::new("strace")
|
|
.args([
|
|
"-f",
|
|
"-e",
|
|
&format!("trace={denied}"),
|
|
"-o",
|
|
trace_path.to_str().unwrap(),
|
|
&exe,
|
|
"run",
|
|
"alpine:3.23",
|
|
"--",
|
|
"true",
|
|
])
|
|
.status()
|
|
.expect("strace run");
|
|
assert!(out.success(), "the traced run failed; see the strace log");
|
|
let trace = std::fs::read_to_string(&trace_path).unwrap();
|
|
let _ = std::fs::remove_file(&trace_path);
|
|
for line in trace.lines() {
|
|
let denied_call = denied.split(',').any(|syscall| line.contains(syscall));
|
|
let failed = line.contains("= -1 EPERM") || line.contains("= -1 ENOSYS");
|
|
assert!(
|
|
!(denied_call && failed),
|
|
"denied syscall in the trace: {line}"
|
|
);
|
|
}
|
|
}
|
|
}
|