mirror of
https://github.com/edera-dev/krata.git
synced 2025-08-03 05:10:55 +00:00
krata: reorganize crates
This commit is contained in:
30
crates/kratactl/Cargo.toml
Normal file
30
crates/kratactl/Cargo.toml
Normal 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"
|
153
crates/kratactl/bin/control.rs
Normal file
153
crates/kratactl/bin/control.rs
Normal 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(())
|
||||
}
|
44
crates/kratactl/src/client.rs
Normal file
44
crates/kratactl/src/client.rs
Normal 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))
|
||||
}
|
||||
}
|
57
crates/kratactl/src/console.rs
Normal file
57
crates/kratactl/src/console.rs
Normal 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(())
|
||||
}
|
||||
}
|
2
crates/kratactl/src/lib.rs
Normal file
2
crates/kratactl/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod console;
|
Reference in New Issue
Block a user