2024-03-24 05:25:48 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2024-03-15 15:59:18 +00:00
|
|
|
use anyhow::Result;
|
|
|
|
use clap::Parser;
|
2024-04-12 11:09:26 -07:00
|
|
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
2024-03-27 02:54:39 +00:00
|
|
|
use krata::{
|
|
|
|
events::EventStream,
|
|
|
|
v1::{
|
|
|
|
common::{
|
|
|
|
guest_image_spec::Image, GuestImageSpec, GuestOciImageSpec, GuestSpec, GuestStatus,
|
|
|
|
GuestTaskSpec, GuestTaskSpecEnvVar,
|
|
|
|
},
|
|
|
|
control::{
|
|
|
|
control_service_client::ControlServiceClient, watch_events_reply::Event,
|
2024-04-12 11:09:26 -07:00
|
|
|
CreateGuestRequest, OciProgressEventLayerPhase, OciProgressEventPhase,
|
2024-03-27 02:54:39 +00:00
|
|
|
},
|
2024-03-15 15:59:18 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
use log::error;
|
2024-03-15 17:36:26 +00:00
|
|
|
use tokio::select;
|
2024-03-15 15:59:18 +00:00
|
|
|
use tonic::{transport::Channel, Request};
|
|
|
|
|
2024-03-27 02:54:39 +00:00
|
|
|
use crate::console::StdioConsoleStream;
|
2024-03-15 15:59:18 +00:00
|
|
|
|
|
|
|
#[derive(Parser)]
|
2024-04-05 17:00:02 -07:00
|
|
|
#[command(about = "Launch a new guest")]
|
2024-03-15 15:59:18 +00:00
|
|
|
pub struct LauchCommand {
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(short, long, help = "Name of the guest")]
|
2024-03-15 15:59:18 +00:00
|
|
|
name: Option<String>,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(
|
|
|
|
short,
|
|
|
|
long,
|
|
|
|
default_value_t = 1,
|
|
|
|
help = "vCPUs available to the guest"
|
|
|
|
)]
|
2024-03-15 15:59:18 +00:00
|
|
|
cpus: u32,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(
|
|
|
|
short,
|
|
|
|
long,
|
|
|
|
default_value_t = 512,
|
|
|
|
help = "Memory available to the guest, in megabytes"
|
|
|
|
)]
|
2024-03-15 15:59:18 +00:00
|
|
|
mem: u64,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg[short, long, help = "Environment variables set in the guest"]]
|
2024-03-15 15:59:18 +00:00
|
|
|
env: Option<Vec<String>>,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(
|
|
|
|
short,
|
|
|
|
long,
|
|
|
|
help = "Attach to the guest after guest starts, implies --wait"
|
|
|
|
)]
|
2024-03-15 15:59:18 +00:00
|
|
|
attach: bool,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(
|
|
|
|
short = 'W',
|
|
|
|
long,
|
|
|
|
help = "Wait for the guest to start, implied by --attach"
|
|
|
|
)]
|
2024-03-23 10:09:00 +00:00
|
|
|
wait: bool,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(help = "Container image for guest to use")]
|
2024-03-15 15:59:18 +00:00
|
|
|
oci: String,
|
2024-04-05 17:00:02 -07:00
|
|
|
#[arg(
|
|
|
|
allow_hyphen_values = true,
|
|
|
|
trailing_var_arg = true,
|
|
|
|
help = "Command to run inside the guest"
|
|
|
|
)]
|
|
|
|
command: Vec<String>,
|
2024-03-15 15:59:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl LauchCommand {
|
|
|
|
pub async fn run(
|
|
|
|
self,
|
|
|
|
mut client: ControlServiceClient<Channel>,
|
|
|
|
events: EventStream,
|
|
|
|
) -> Result<()> {
|
|
|
|
let request = CreateGuestRequest {
|
|
|
|
spec: Some(GuestSpec {
|
|
|
|
name: self.name.unwrap_or_default(),
|
|
|
|
image: Some(GuestImageSpec {
|
|
|
|
image: Some(Image::Oci(GuestOciImageSpec { image: self.oci })),
|
|
|
|
}),
|
|
|
|
vcpus: self.cpus,
|
|
|
|
mem: self.mem,
|
2024-03-27 02:54:39 +00:00
|
|
|
task: Some(GuestTaskSpec {
|
|
|
|
environment: env_map(&self.env.unwrap_or_default())
|
|
|
|
.iter()
|
|
|
|
.map(|(key, value)| GuestTaskSpecEnvVar {
|
|
|
|
key: key.clone(),
|
|
|
|
value: value.clone(),
|
|
|
|
})
|
|
|
|
.collect(),
|
2024-04-05 17:00:02 -07:00
|
|
|
command: self.command,
|
2024-03-27 02:54:39 +00:00
|
|
|
}),
|
|
|
|
annotations: vec![],
|
2024-03-15 15:59:18 +00:00
|
|
|
}),
|
|
|
|
};
|
|
|
|
let response = client
|
|
|
|
.create_guest(Request::new(request))
|
|
|
|
.await?
|
|
|
|
.into_inner();
|
|
|
|
let id = response.guest_id;
|
2024-03-23 10:09:00 +00:00
|
|
|
|
|
|
|
if self.wait || self.attach {
|
2024-03-15 15:59:18 +00:00
|
|
|
wait_guest_started(&id, events.clone()).await?;
|
2024-03-23 10:09:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let code = if self.attach {
|
2024-03-15 15:59:18 +00:00
|
|
|
let input = StdioConsoleStream::stdin_stream(id.clone()).await;
|
|
|
|
let output = client.console_data(input).await?.into_inner();
|
2024-03-15 17:36:26 +00:00
|
|
|
let stdout_handle =
|
|
|
|
tokio::task::spawn(async move { StdioConsoleStream::stdout(output).await });
|
2024-03-15 15:59:18 +00:00
|
|
|
let exit_hook_task = StdioConsoleStream::guest_exit_hook(id.clone(), events).await?;
|
2024-03-15 17:36:26 +00:00
|
|
|
select! {
|
|
|
|
x = stdout_handle => {
|
|
|
|
x??;
|
|
|
|
None
|
|
|
|
},
|
|
|
|
x = exit_hook_task => x?
|
|
|
|
}
|
2024-03-15 15:59:18 +00:00
|
|
|
} else {
|
2024-03-23 10:09:00 +00:00
|
|
|
println!("{}", id);
|
2024-03-15 17:36:26 +00:00
|
|
|
None
|
|
|
|
};
|
2024-03-21 22:49:37 -07:00
|
|
|
StdioConsoleStream::restore_terminal_mode();
|
2024-03-15 17:36:26 +00:00
|
|
|
std::process::exit(code.unwrap_or(0));
|
2024-03-15 15:59:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn wait_guest_started(id: &str, events: EventStream) -> Result<()> {
|
|
|
|
let mut stream = events.subscribe();
|
2024-04-12 11:09:26 -07:00
|
|
|
let mut multi_progress: Option<(MultiProgress, HashMap<String, ProgressBar>)> = None;
|
2024-03-15 15:59:18 +00:00
|
|
|
while let Ok(event) = stream.recv().await {
|
|
|
|
match event {
|
|
|
|
Event::GuestChanged(changed) => {
|
2024-04-12 11:09:26 -07:00
|
|
|
if let Some((multi_progress, _)) = multi_progress.as_mut() {
|
|
|
|
let _ = multi_progress.clear();
|
|
|
|
}
|
|
|
|
|
2024-03-15 15:59:18 +00:00
|
|
|
let Some(guest) = changed.guest else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
if guest.id != id {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let Some(state) = guest.state else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(ref error) = state.error_info {
|
2024-03-23 07:00:12 +00:00
|
|
|
if state.status() == GuestStatus::Failed {
|
|
|
|
error!("launch failed: {}", error.message);
|
|
|
|
std::process::exit(1);
|
|
|
|
} else {
|
|
|
|
error!("guest error: {}", error.message);
|
|
|
|
}
|
2024-03-15 15:59:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if state.status() == GuestStatus::Destroyed {
|
|
|
|
error!("guest destroyed");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if state.status() == GuestStatus::Started {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2024-04-12 11:09:26 -07:00
|
|
|
|
|
|
|
Event::OciProgress(oci) => {
|
|
|
|
if multi_progress.is_none() {
|
|
|
|
multi_progress = Some((MultiProgress::new(), HashMap::new()));
|
|
|
|
}
|
|
|
|
|
|
|
|
let Some((multi_progress, progresses)) = multi_progress.as_mut() else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
match oci.phase() {
|
|
|
|
OciProgressEventPhase::Resolved
|
|
|
|
| OciProgressEventPhase::ConfigAcquire
|
|
|
|
| OciProgressEventPhase::LayerAcquire => {
|
|
|
|
if progresses.is_empty() && !oci.layers.is_empty() {
|
|
|
|
for layer in &oci.layers {
|
|
|
|
let bar = ProgressBar::new(layer.total);
|
|
|
|
bar.set_style(
|
2024-04-14 17:19:38 -07:00
|
|
|
ProgressStyle::with_template("{msg} {wide_bar}").unwrap(),
|
2024-04-12 11:09:26 -07:00
|
|
|
);
|
|
|
|
progresses.insert(layer.id.clone(), bar.clone());
|
|
|
|
multi_progress.add(bar);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for layer in oci.layers {
|
|
|
|
let Some(progress) = progresses.get_mut(&layer.id) else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
let phase = match layer.phase() {
|
|
|
|
OciProgressEventLayerPhase::Waiting => "waiting",
|
|
|
|
OciProgressEventLayerPhase::Downloading => "downloading",
|
|
|
|
OciProgressEventLayerPhase::Downloaded => "downloaded",
|
|
|
|
OciProgressEventLayerPhase::Extracting => "extracting",
|
|
|
|
OciProgressEventLayerPhase::Extracted => "extracted",
|
|
|
|
_ => "unknown",
|
|
|
|
};
|
|
|
|
|
2024-04-14 17:19:38 -07:00
|
|
|
let simple = if let Some((_, hash)) = layer.id.split_once(':') {
|
|
|
|
hash
|
|
|
|
} else {
|
|
|
|
id
|
|
|
|
};
|
|
|
|
let simple = if simple.len() > 10 {
|
|
|
|
&simple[0..10]
|
|
|
|
} else {
|
|
|
|
simple
|
|
|
|
};
|
|
|
|
let message = format!("{:width$} {}", simple, phase, width = 10);
|
|
|
|
|
|
|
|
if message != progress.message() {
|
|
|
|
progress.set_message(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
progress.update(|state| {
|
|
|
|
state.set_len(layer.total);
|
|
|
|
state.set_pos(layer.value);
|
|
|
|
});
|
2024-04-12 11:09:26 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
OciProgressEventPhase::Packing => {
|
2024-04-14 17:19:38 -07:00
|
|
|
for (key, bar) in &mut *progresses {
|
2024-04-12 11:09:26 -07:00
|
|
|
if key == "packing" {
|
|
|
|
continue;
|
|
|
|
}
|
2024-04-14 17:19:38 -07:00
|
|
|
bar.finish_and_clear();
|
|
|
|
multi_progress.remove(bar);
|
2024-04-12 11:09:26 -07:00
|
|
|
}
|
|
|
|
progresses.retain(|k, _| k == "packing");
|
|
|
|
if progresses.is_empty() {
|
|
|
|
let progress = ProgressBar::new(100);
|
2024-04-14 17:19:38 -07:00
|
|
|
progress.set_message("packing");
|
2024-04-12 11:09:26 -07:00
|
|
|
progress.set_style(
|
2024-04-14 17:19:38 -07:00
|
|
|
ProgressStyle::with_template("{msg} {wide_bar}").unwrap(),
|
2024-04-12 11:09:26 -07:00
|
|
|
);
|
|
|
|
progresses.insert("packing".to_string(), progress);
|
|
|
|
}
|
|
|
|
let Some(progress) = progresses.get("packing") else {
|
|
|
|
continue;
|
|
|
|
};
|
2024-04-14 17:19:38 -07:00
|
|
|
|
|
|
|
progress.update(|state| {
|
|
|
|
state.set_len(oci.total);
|
|
|
|
state.set_pos(oci.value);
|
|
|
|
});
|
2024-04-12 11:09:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
|
|
|
|
for progress in progresses {
|
|
|
|
progress.1.tick();
|
|
|
|
}
|
|
|
|
}
|
2024-03-15 15:59:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2024-03-24 05:25:48 +00:00
|
|
|
|
|
|
|
fn env_map(env: &[String]) -> HashMap<String, String> {
|
|
|
|
let mut map = HashMap::<String, String>::new();
|
|
|
|
for item in env {
|
|
|
|
if let Some((key, value)) = item.split_once('=') {
|
|
|
|
map.insert(key.to_string(), value.to_string());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
map
|
|
|
|
}
|