265 lines
7.6 KiB
Rust
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"),
|
|
}
|
|
}
|
|
}
|