use arcbox_cli::client::{
self, CreateContainerRequest, CreateContainerResponse, ContainerWaitResponse,
HostConfig, PortBinding,
};
use anyhow::Result;
use clap::Args;
use std::collections::HashMap;
#[derive(Args)]
pub struct RunArgs {
pub image: String,
#[arg(trailing_var_arg = true)]
pub command: Vec<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(short, long)]
pub detach: bool,
#[arg(long)]
pub rm: bool,
#[arg(short, long)]
pub tty: bool,
#[arg(short, long)]
pub interactive: bool,
#[arg(short, long)]
pub env: Vec<String>,
#[arg(short, long)]
pub volume: Vec<String>,
#[arg(short, long)]
pub publish: Vec<String>,
#[arg(short, long)]
pub workdir: Option<String>,
}
pub async fn execute(args: RunArgs) -> Result<()> {
let daemon = client::get_client().await?;
let host_config = build_host_config(&args);
let request = CreateContainerRequest {
image: args.image.clone(),
cmd: args.command.clone(),
env: args.env.clone(),
working_dir: args.workdir.clone(),
tty: args.tty,
open_stdin: args.interactive,
attach_stdin: args.interactive && !args.detach,
attach_stdout: !args.detach,
attach_stderr: !args.detach,
host_config: Some(host_config),
..Default::default()
};
let name_param = args
.name
.as_ref()
.map(|n| format!("?name={}", n))
.unwrap_or_default();
let path = format!("/v1.43/containers/create{}", name_param);
let response: CreateContainerResponse = daemon.post(&path, Some(&request)).await?;
let container_id = response.id;
for warning in &response.warnings {
eprintln!("WARNING: {}", warning);
}
let start_path = format!("/v1.43/containers/{}/start", container_id);
daemon.post_empty::<()>(&start_path, None).await?;
if args.detach {
println!("{}", client::short_id(&container_id));
} else {
if args.tty || args.interactive {
tracing::warn!("Interactive/TTY mode not fully implemented yet");
}
let wait_path = format!("/v1.43/containers/{}/wait", container_id);
let wait_response: ContainerWaitResponse = daemon.post(&wait_path, None::<()>).await?;
let logs_path = format!(
"/v1.43/containers/{}/logs?stdout=true&stderr=true",
container_id
);
if let Ok(logs) = daemon.get_raw(&logs_path).await {
print_container_logs(&logs);
}
if args.rm {
let rm_path = format!("/v1.43/containers/{}", container_id);
let _ = daemon.delete(&rm_path).await;
}
if wait_response.status_code != 0 {
std::process::exit(wait_response.status_code as i32);
}
}
Ok(())
}
fn build_host_config(args: &RunArgs) -> HostConfig {
let mut host_config = HostConfig {
binds: args.volume.clone(),
auto_remove: args.rm && args.detach,
..Default::default()
};
if !args.publish.is_empty() {
let mut port_bindings: HashMap<String, Vec<PortBinding>> = HashMap::new();
for publish in &args.publish {
if let Some((host, container)) = parse_port_mapping(publish) {
let container_port = format!("{}/tcp", container);
port_bindings.entry(container_port).or_default().push(PortBinding {
host_ip: String::new(),
host_port: host.to_string(),
});
}
}
if !port_bindings.is_empty() {
host_config.port_bindings = Some(port_bindings);
}
}
host_config
}
fn parse_port_mapping(s: &str) -> Option<(u16, u16)> {
let parts: Vec<&str> = s.split(':').collect();
match parts.len() {
2 => {
let host_port = parts[0].parse().ok()?;
let container_port = parts[1].parse().ok()?;
Some((host_port, container_port))
}
3 => {
let host_port = parts[1].parse().ok()?;
let container_port = parts[2].parse().ok()?;
Some((host_port, container_port))
}
_ => None,
}
}
fn print_container_logs(data: &[u8]) {
let mut offset = 0;
while offset + 8 <= data.len() {
let size = u32::from_be_bytes([
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]) as usize;
let end = offset + 8 + size;
if end > data.len() {
break;
}
let content = &data[offset + 8..end];
if let Ok(s) = std::str::from_utf8(content) {
print!("{}", s);
}
offset = end;
}
if offset == 0 && !data.is_empty() {
if let Ok(s) = std::str::from_utf8(data) {
print!("{}", s);
}
}
}