Files
sausage/userland/box/src/cli.rs
T

265 lines
7.6 KiB
Rust

//! Command arguments and presentation. Raw execution archives never depend on the inline budget.
use crate::*;
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "box", about = "Disposable build and test boxes")]
pub struct Args {
#[arg(long, global = true, help = "Containers service hostname[:port]")]
pub host: Option<String>,
#[command(subcommand)]
pub command: Operation,
}
#[derive(Subcommand, Debug)]
pub enum Operation {
Create {
#[arg(long)]
flake: Option<String>,
#[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
ttl: Option<u32>,
},
Exec {
id: String,
#[arg(long, default_value_t = 60, value_parser = clap::value_parser!(u32).range(1..=1800))]
timeout: u32,
#[arg(long, default_value_t = excerpt::DEFAULT_BYTES)]
inline_bytes: u32,
#[arg(last = true, required = true)]
cmd: Vec<String>,
},
Put {
id: String,
world_path: String,
box_path: String,
},
Get {
id: String,
box_path: String,
world_path: String,
},
Logs {
id: String,
},
Ls,
Rm {
id: String,
},
}
fn notices(result: &Executed) {
for (name, path) in [
("stdout", &result.stdout_path),
("stderr", &result.stderr_path),
("result", &result.output_log),
] {
if let Some(path) = path {
eprintln!("box {name} archive: {path}");
}
}
if let Some(notice) = &result.output_notice {
eprintln!("box: {notice}");
}
if result.timed_out {
eprintln!("box: command timed out");
}
}
pub fn run(args: Args) -> Result<i32> {
let h = args.host;
let service = BoxPlugin;
match args.command {
Operation::Create { flake, ttl } => {
let config = Config {
nixos: flake.is_none().then(|| Nixos::Text(NODE_MODULE.into())),
flake,
system: None,
};
let c = service.create(Create {
config,
ttl_secs: ttl,
owner: Some("box CLI".into()),
host: h,
})?;
println!("{}", c.id);
if c.state != "running" {
if let Some(tail) = c.build_log_tail {
eprintln!("{tail}");
}
if let Some(path) = c.build_log_path {
eprintln!("box build archive: {path}");
}
return Err(error(format!(
"box {} failed: {}; remove with box rm",
c.id,
c.error.unwrap_or_default()
)));
}
}
Operation::Exec {
id,
timeout,
inline_bytes,
cmd,
} => {
let result = service.exec(Exec {
id,
cmd: Command::Argv(cmd),
stdin: None,
timeout_secs: Some(timeout),
inline_bytes: Some(inline_bytes),
host: h,
})?;
std::io::stdout().write_all(result.stdout.as_bytes())?;
std::io::stderr().write_all(result.stderr.as_bytes())?;
notices(&result);
return Ok(result.exit);
}
Operation::Put {
id,
world_path,
box_path,
} => {
if Path::new(&world_path).is_dir() {
service.put_tree(PutTree {
id,
from_vfs_dir: world_path,
to_dir: box_path,
host: h,
})?;
} else {
service.put_file(PutFile {
id,
path: box_path,
text: None,
from_vfs: Some(world_path),
host: h,
})?;
}
}
Operation::Get {
id,
box_path,
world_path,
} => {
let host = resolve(&id, h.as_deref())?;
let kind = checked(
&host,
&id,
json!([
"sh",
"-c",
"if [ -d \"$1\" ]; then printf directory; elif [ -f \"$1\" ]; then printf file; else exit 1; fi",
"sh",
box_path
]),
)?;
if kind == "directory" {
service.get_tree(GetTree {
id,
from_dir: box_path,
to_vfs_dir: world_path,
host: h,
})?;
} else {
service.get_file(GetFile {
id,
path: box_path,
to_vfs: Some(world_path),
host: h,
})?;
}
}
Operation::Logs { id } => {
let log = service.log(Id { id, host: h })?;
print!("{}", log.text);
eprintln!("box build archive: {} ({} bytes)", log.path, log.bytes);
if let Some(notice) = log.notice {
eprintln!("box: {notice}");
}
}
Operation::Ls => {
let list = service.list(List { host: h })?;
for c in list.containers {
println!("{}\t{}\t{}", c.id, c.state, c.expires_at);
}
}
Operation::Rm { id } => {
service.delete(Id { id, host: h })?;
}
}
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn argument_forms_and_validation() {
for args in [
vec!["box", "create"],
vec![
"box",
"create",
"--flake",
"github:example/project#container",
"--ttl",
"600",
],
vec![
"box",
"exec",
"tc-abc123",
"--timeout",
"5",
"--",
"sh",
"-c",
"exit 7",
],
vec!["box", "put", "tc-abc123", "dir", "/work"],
vec!["box", "get", "tc-abc123", "/work", "out"],
vec!["box", "logs", "tc-abc123"],
vec!["box", "ls"],
vec!["box", "rm", "tc-abc123"],
vec!["box", "--host", "localhost:8093", "ls"],
] {
assert!(Args::try_parse_from(args).is_ok());
}
for args in [
vec!["box", "create", "--ttl", "0"],
vec!["box", "exec", "tc-abc123", "--"],
vec![
"box",
"exec",
"tc-abc123",
"--timeout",
"1801",
"--",
"true",
],
vec!["box", "exec", "tc-abc123", "true"],
vec!["box", "put", "tc-abc123", "a"],
vec!["box", "unknown"],
] {
assert!(Args::try_parse_from(args).is_err());
}
let args = Args::try_parse_from([
"box",
"exec",
"tc-abc123",
"--inline-bytes",
"0",
"--",
"echo",
"--flag",
"space here",
])
.unwrap();
match args.command {
Operation::Exec {
cmd, inline_bytes, ..
} => {
assert_eq!(cmd, ["echo", "--flag", "space here"]);
assert_eq!(inline_bytes, 0);
}
_ => panic!("wrong operation"),
}
}
}