use crate::cli::{
CapabilityFlags, GlobalFlags, KernelFlags, ManagementFlags, NetworkFlags, ProcessFlags,
PublishFlags, ResourceFlags, VolumeFlags,
};
use crate::terminal::StreamManager;
use crate::util::to_shell_exit_code;
use boxlite::{BoxOptions, BoxliteRuntime, LiteBox, RootfsSpec};
use clap::Args;
use std::io::{self, IsTerminal};
#[derive(Args, Debug)]
pub struct RunArgs {
#[command(flatten)]
pub process: ProcessFlags,
#[command(flatten)]
pub resource: ResourceFlags,
#[command(flatten)]
pub capability: CapabilityFlags,
#[command(flatten)]
pub publish: PublishFlags,
#[command(flatten)]
pub volume: VolumeFlags,
#[command(flatten)]
pub network: NetworkFlags,
#[command(flatten)]
pub management: ManagementFlags,
#[arg(long = "rootfs", value_name = "PATH")]
pub rootfs: Option<String>,
#[arg(index = 1, trailing_var_arg = true, value_name = "IMAGE|COMMAND")]
pub args: Vec<String>,
#[command(flatten, next_help_heading = "Advanced boot options")]
pub boot: KernelFlags,
}
pub async fn execute(args: RunArgs, global: &GlobalFlags) -> anyhow::Result<i32> {
args.boot.require_enabled(global.experimental_features())?;
args.management
.require_enabled(global.experimental_features())?;
let (rootfs, command_args) = args.rootfs_and_command()?;
let command_args = command_args.to_vec();
let mut runner = BoxRunner::new(args, global)?;
runner.run(rootfs, command_args).await
}
struct BoxRunner {
args: RunArgs,
rt: BoxliteRuntime,
home: Option<std::path::PathBuf>,
}
impl BoxRunner {
fn new(args: RunArgs, global: &GlobalFlags) -> anyhow::Result<Self> {
let rt = global.create_runtime()?;
let home = global.home.clone();
Ok(Self { args, rt, home })
}
async fn run(&mut self, rootfs: RootfsSpec, command_args: Vec<String>) -> anyhow::Result<i32> {
self.validate_flags()?;
let litebox = self.create_box(rootfs, &command_args).await?;
if self.args.management.detach {
litebox.start().await?;
println!("{}", litebox.id());
return Ok(0);
}
let mut execution = litebox.attach(None).await?;
litebox.start().await?;
if self.args.process.tty {
self.args.process.interactive = true;
}
let streamer = StreamManager::new(
&mut execution,
self.args.process.interactive,
self.args.process.tty,
);
let exit_code = streamer.start().await?;
Ok(to_shell_exit_code(exit_code))
}
async fn create_box(
&self,
rootfs: RootfsSpec,
command_args: &[String],
) -> anyhow::Result<LiteBox> {
let mut options = BoxOptions::default();
self.args.resource.apply_to(&mut options);
self.args.capability.apply_to(&mut options);
self.args.boot.apply_to(&mut options);
self.args.management.apply_to(&mut options)?;
self.args.publish.apply_to(&mut options)?;
self.args
.volume
.apply_to(&mut options, self.home.as_deref())?;
self.args.network.apply_to(&mut options)?;
self.args.process.apply_to(&mut options)?;
if self.args.management.detach {
options.auto_delete = Some(0);
}
if !command_args.is_empty() {
options.cmd = Some(command_args.to_vec());
}
options.rootfs = rootfs;
let litebox = self
.rt
.create(options, self.args.management.name.clone())
.await?;
Ok(litebox)
}
fn validate_flags(&self) -> anyhow::Result<()> {
if self.args.process.tty && !io::stdin().is_terminal() {
anyhow::bail!("the input device is not a TTY.");
}
Ok(())
}
}
impl RunArgs {
fn rootfs_and_command(&self) -> anyhow::Result<(RootfsSpec, &[String])> {
resolve_rootfs_and_command(self.rootfs.as_deref(), &self.args)
}
}
fn resolve_rootfs_and_command<'a>(
rootfs: Option<&str>,
args: &'a [String],
) -> anyhow::Result<(RootfsSpec, &'a [String])> {
if let Some(path) = rootfs {
return Ok((RootfsSpec::RootfsPath(path.to_string()), args));
}
let Some((image, command)) = args.split_first() else {
anyhow::bail!("provide IMAGE or --rootfs PATH");
};
Ok((RootfsSpec::Image(image.clone()), command))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{Cli, Commands};
use clap::Parser;
#[test]
fn run_capability_flags_are_repeatable() {
let cli = Cli::try_parse_from([
"boxlite",
"run",
"--cap-add",
"SYS_ADMIN",
"--cap-add",
"NET_ADMIN",
"--cap-drop",
"CAP_NET_RAW",
"alpine",
])
.expect("capability flags should parse");
let Commands::Run(args) = cli.command else {
panic!("expected run command");
};
assert_eq!(args.capability.cap_add, vec!["SYS_ADMIN", "NET_ADMIN"]);
assert_eq!(args.capability.cap_drop, vec!["CAP_NET_RAW"]);
}
#[test]
fn run_rootfs_flag_sets_rootfs_path_and_uses_trailing_command() {
let cli = Cli::try_parse_from(["boxlite", "run", "--rootfs", "/tmp/rootfs", "echo", "hi"])
.expect("run --rootfs should parse");
let Commands::Run(args) = cli.command else {
panic!("expected run command");
};
let (rootfs, command) = args
.rootfs_and_command()
.expect("rootfs command should resolve");
match rootfs {
RootfsSpec::RootfsPath(path) => assert_eq!(path, "/tmp/rootfs"),
other => panic!("expected RootfsPath, got {other:?}"),
}
assert_eq!(command, &["echo".to_string(), "hi".to_string()]);
}
#[test]
fn run_rootfs_without_command_leaves_command_empty() {
let cli = Cli::try_parse_from(["boxlite", "run", "--rootfs", "/tmp/rootfs"])
.expect("run --rootfs should parse");
let Commands::Run(args) = cli.command else {
panic!("expected run command");
};
let (rootfs, command) = args
.rootfs_and_command()
.expect("rootfs command should resolve");
match rootfs {
RootfsSpec::RootfsPath(path) => assert_eq!(path, "/tmp/rootfs"),
other => panic!("expected RootfsPath, got {other:?}"),
}
assert!(command.is_empty());
}
#[test]
fn run_without_rootfs_preserves_image_and_command() {
let cli = Cli::try_parse_from(["boxlite", "run", "alpine:latest", "echo", "hi"])
.expect("run image command should parse");
let Commands::Run(args) = cli.command else {
panic!("expected run command");
};
let (rootfs, command) = args
.rootfs_and_command()
.expect("image command should resolve");
match rootfs {
RootfsSpec::Image(image) => assert_eq!(image, "alpine:latest"),
other => panic!("expected Image, got {other:?}"),
}
assert_eq!(command, &["echo".to_string(), "hi".to_string()]);
}
#[test]
fn run_requires_image_or_rootfs() {
let cli = Cli::try_parse_from(["boxlite", "run"]).expect("run should parse");
let Commands::Run(args) = cli.command else {
panic!("expected run command");
};
let err = args
.rootfs_and_command()
.expect_err("missing source must be rejected");
assert!(err.to_string().contains("IMAGE or --rootfs"));
}
}