use crate::cli::{
CapabilityFlags, GlobalFlags, KernelFlags, NetworkFlags, PublishFlags, ResourceFlags,
VolumeFlags,
};
use boxlite::{BoxOptions, RootfsSpec};
use clap::Args;
#[derive(Args, Debug)]
pub struct CreateArgs {
#[arg(index = 1)]
pub image: Option<String>,
#[arg(long = "rootfs", value_name = "PATH")]
pub rootfs: Option<String>,
#[command(flatten)]
pub management: crate::cli::ManagementFlags,
#[arg(short = 'e', long = "env")]
pub env: Vec<String>,
#[arg(short = 'w', long = "workdir")]
pub workdir: Option<String>,
#[arg(long = "entrypoint", value_name = "EXEC")]
pub entrypoint: Option<String>,
#[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,
#[arg(index = 2, trailing_var_arg = true)]
pub command: Vec<String>,
#[command(flatten, next_help_heading = "Advanced boot options")]
pub boot: KernelFlags,
}
pub async fn execute(args: CreateArgs, global: &GlobalFlags) -> anyhow::Result<()> {
let box_options = args.to_box_options(global)?;
let rt = global.create_runtime()?;
let litebox = rt.create(box_options, args.management.name.clone()).await?;
println!("{}", litebox.id());
Ok(())
}
impl CreateArgs {
fn to_box_options(&self, global: &GlobalFlags) -> anyhow::Result<BoxOptions> {
self.boot.require_enabled(global.experimental_features())?;
self.management
.require_enabled(global.experimental_features())?;
self.management.require_sweeper(global.targets_rest()?)?;
let mut options = BoxOptions::default();
self.resource.apply_to(&mut options);
self.capability.apply_to(&mut options);
self.boot.apply_to(&mut options);
self.management.apply_to(&mut options)?;
self.publish.apply_to(&mut options)?;
self.volume.apply_to(&mut options, global.home.as_deref())?;
self.network.apply_to(&mut options)?;
options.detach = true;
options.working_dir = self.workdir.clone();
if let Some(ref exec) = self.entrypoint {
options.entrypoint = Some(vec![exec.clone()]);
}
if !self.command.is_empty() {
options.cmd = Some(self.command.clone());
}
crate::cli::apply_env_vars(&self.env, &mut options);
options.rootfs = self.rootfs_spec()?;
Ok(options)
}
fn rootfs_spec(&self) -> anyhow::Result<RootfsSpec> {
match (self.image.as_ref(), self.rootfs.as_ref()) {
(Some(image), None) => Ok(RootfsSpec::Image(image.clone())),
(None, Some(path)) => Ok(RootfsSpec::RootfsPath(path.clone())),
(None, None) => anyhow::bail!("provide IMAGE or --rootfs PATH"),
(Some(_), Some(_)) => anyhow::bail!("provide either IMAGE or --rootfs PATH, not both"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{Cli, Commands};
use clap::Parser;
#[test]
fn an_explicit_delete_deadline_survives_creates_detach_default() {
let cli = Cli::try_parse_from([
"boxlite",
"--url",
"http://127.0.0.1:1",
"create",
"--auto-delete",
"1h",
"alpine:latest",
])
.expect("create --auto-delete should parse");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
let opts = args
.to_box_options(&cli.global)
.expect("options should build");
assert!(opts.detach, "create always detaches");
assert_eq!(opts.auto_delete, Some(3_600));
}
#[test]
fn create_without_a_deadline_disables_remove_on_stop() {
let opts = {
let cli = Cli::try_parse_from(["boxlite", "create", "alpine:latest"])
.expect("create should parse");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
args.to_box_options(&cli.global)
.expect("options should build")
};
assert_eq!(
opts.auto_delete,
Some(0),
"a detached box cannot remove on stop, so the sentinel is cleared"
);
}
#[test]
fn create_rootfs_flag_sets_rootfs_path() {
let cli = Cli::try_parse_from(["boxlite", "create", "--rootfs", "/tmp/rootfs"])
.expect("create --rootfs should parse");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
let opts = args
.to_box_options(&cli.global)
.expect("rootfs options should build");
match opts.rootfs {
RootfsSpec::RootfsPath(path) => assert_eq!(path, "/tmp/rootfs"),
other => panic!("expected RootfsPath, got {other:?}"),
}
}
#[test]
fn create_requires_image_or_rootfs() {
let cli = Cli::try_parse_from(["boxlite", "create"]).expect("create should parse");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
let err = args
.to_box_options(&cli.global)
.expect_err("missing source must be rejected");
assert!(err.to_string().contains("IMAGE or --rootfs"));
}
#[test]
fn create_rejects_image_and_rootfs_together() {
let cli = Cli::try_parse_from(["boxlite", "create", "--rootfs", "/tmp/rootfs", "alpine"])
.expect("create image and rootfs should parse");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
let err = args
.to_box_options(&cli.global)
.expect_err("competing sources must be rejected");
assert!(err.to_string().contains("either IMAGE or --rootfs"));
}
#[test]
fn create_capability_flags_reach_box_options() {
let cli = Cli::try_parse_from([
"boxlite",
"create",
"--cap-add",
"SYS_ADMIN",
"--cap-drop",
"CAP_NET_RAW",
"alpine",
])
.expect("capability flags should parse");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
let opts = args
.to_box_options(&cli.global)
.expect("options should build");
let capabilities = opts.advanced.capabilities().expect("capabilities set");
assert_eq!(capabilities.add, vec!["SYS_ADMIN"]);
assert_eq!(capabilities.drop, vec!["CAP_NET_RAW"]);
}
}