krata: reorganize crates

This commit is contained in:
Alex Zenla
2024-03-07 18:12:47 +00:00
parent c0eeab4047
commit 7bc0c95f00
97 changed files with 24 additions and 24 deletions

View File

@ -0,0 +1,30 @@
[package]
name = "kratactl"
version.workspace = true
edition = "2021"
resolver = "2"
[dependencies]
anyhow = { workspace = true }
async-stream = { workspace = true }
clap = { workspace = true }
cli-tables = { workspace = true }
env_logger = { workspace = true }
krata = { path = "../krata" }
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
termion = { workspace = true }
tokio = { workspace = true }
tokio-native-tls = { workspace = true }
tokio-stream = { workspace = true }
tonic = { workspace = true }
tower = { workspace = true }
url = { workspace = true }
[lib]
name = "kratactl"
[[bin]]
name = "kratactl"
path = "bin/control.rs"

View File

@ -0,0 +1,153 @@
use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use env_logger::Env;
use krata::control::{
watch_events_reply::Event, DestroyGuestRequest, LaunchGuestRequest, ListGuestsRequest,
WatchEventsRequest,
};
use kratactl::{client::ControlClientProvider, console::StdioConsoleStream};
use tonic::Request;
#[derive(Parser, Debug)]
#[command(version, about)]
struct ControllerArgs {
#[arg(short, long, default_value = "unix:///var/lib/krata/daemon.socket")]
connection: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
List {},
Launch {
#[arg(short, long, default_value_t = 1)]
cpus: u32,
#[arg(short, long, default_value_t = 512)]
mem: u64,
#[arg[short, long]]
env: Option<Vec<String>>,
#[arg(short, long)]
attach: bool,
#[arg()]
image: String,
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
run: Vec<String>,
},
Destroy {
#[arg()]
guest: String,
},
Console {
#[arg()]
guest: String,
},
Watch {},
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init();
let args = ControllerArgs::parse();
let mut client = ControlClientProvider::dial(args.connection.parse()?).await?;
match args.command {
Commands::Launch {
image,
cpus,
mem,
attach,
env,
run,
} => {
let request = LaunchGuestRequest {
image,
vcpus: cpus,
mem,
env: env.unwrap_or_default(),
run,
};
let response = client
.launch_guest(Request::new(request))
.await?
.into_inner();
let Some(guest) = response.guest else {
return Err(anyhow!(
"control service did not return a guest in the response"
));
};
println!("launched guest: {}", guest.id);
if attach {
let input = StdioConsoleStream::stdin_stream(guest.id).await;
let output = client.console_data(input).await?.into_inner();
StdioConsoleStream::stdout(output).await?;
}
}
Commands::Destroy { guest } => {
let _ = client
.destroy_guest(Request::new(DestroyGuestRequest {
guest_id: guest.clone(),
}))
.await?
.into_inner();
println!("destroyed guest: {}", guest);
}
Commands::Console { guest } => {
let input = StdioConsoleStream::stdin_stream(guest).await;
let output = client.console_data(input).await?.into_inner();
StdioConsoleStream::stdout(output).await?;
}
Commands::List { .. } => {
let response = client
.list_guests(Request::new(ListGuestsRequest {}))
.await?
.into_inner();
let mut table = cli_tables::Table::new();
let header = vec!["uuid", "ipv4", "ipv6", "image"];
table.push_row(&header)?;
for guest in response.guests {
table.push_row_string(&vec![guest.id, guest.ipv4, guest.ipv6, guest.image])?;
}
if table.num_records() == 1 {
println!("no guests have been launched");
} else {
println!("{}", table.to_string());
}
}
Commands::Watch {} => {
let response = client
.watch_events(Request::new(WatchEventsRequest {}))
.await?;
let mut stream = response.into_inner();
while let Some(reply) = stream.message().await? {
let Some(event) = reply.event else {
continue;
};
match event {
Event::GuestLaunched(launched) => {
println!("event=guest.launched guest={}", launched.guest_id);
}
Event::GuestDestroyed(destroyed) => {
println!("event=guest.destroyed guest={}", destroyed.guest_id);
}
Event::GuestExited(exited) => {
println!(
"event=guest.exited guest={} code={}",
exited.guest_id, exited.code
);
}
}
}
}
}
Ok(())
}

View File

@ -0,0 +1,44 @@
use anyhow::Result;
use krata::{control::control_service_client::ControlServiceClient, dial::ControlDialAddress};
use tokio::net::UnixStream;
use tonic::transport::{Channel, ClientTlsConfig, Endpoint, Uri};
use tower::service_fn;
pub struct ControlClientProvider {}
impl ControlClientProvider {
pub async fn dial(addr: ControlDialAddress) -> Result<ControlServiceClient<Channel>> {
let channel = match addr {
ControlDialAddress::UnixSocket { path } => {
// This URL is not actually used but is required to be specified.
Endpoint::try_from(format!("unix://localhost/{}", path))?
.connect_with_connector(service_fn(|uri: Uri| {
let path = uri.path().to_string();
UnixStream::connect(path)
}))
.await?
}
ControlDialAddress::Tcp { host, port } => {
Endpoint::try_from(format!("http://{}:{}", host, port))?
.connect()
.await?
}
ControlDialAddress::Tls {
host,
port,
insecure: _,
} => {
let tls_config = ClientTlsConfig::new().domain_name(&host);
let address = format!("https://{}:{}", host, port);
Channel::from_shared(address)?
.tls_config(tls_config)?
.connect()
.await?
}
};
Ok(ControlServiceClient::new(channel))
}
}

View File

@ -0,0 +1,57 @@
use std::{
io::stdout,
os::fd::{AsRawFd, FromRawFd},
};
use anyhow::Result;
use async_stream::stream;
use krata::control::{ConsoleDataReply, ConsoleDataRequest};
use log::debug;
use termion::raw::IntoRawMode;
use tokio::{
fs::File,
io::{stdin, AsyncReadExt, AsyncWriteExt},
};
use tokio_stream::{Stream, StreamExt};
use tonic::Streaming;
pub struct StdioConsoleStream;
impl StdioConsoleStream {
pub async fn stdin_stream(guest: String) -> impl Stream<Item = ConsoleDataRequest> {
let mut stdin = stdin();
stream! {
yield ConsoleDataRequest { guest_id: guest, data: vec![] };
let mut buffer = vec![0u8; 60];
loop {
let size = match stdin.read(&mut buffer).await {
Ok(size) => size,
Err(error) => {
debug!("failed to read stdin: {}", error);
break;
}
};
let data = buffer[0..size].to_vec();
if size == 1 && buffer[0] == 0x1d {
break;
}
yield ConsoleDataRequest { guest_id: String::default(), data };
}
}
}
pub async fn stdout(mut stream: Streaming<ConsoleDataReply>) -> Result<()> {
let terminal = stdout().into_raw_mode()?;
let mut stdout = unsafe { File::from_raw_fd(terminal.as_raw_fd()) };
while let Some(reply) = stream.next().await {
let reply = reply?;
if reply.data.is_empty() {
continue;
}
stdout.write_all(&reply.data).await?;
stdout.flush().await?;
}
Ok(())
}
}

View File

@ -0,0 +1,2 @@
pub mod client;
pub mod console;