diff --git a/Cargo.toml b/Cargo.toml index 900f0f1..c6bfa73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,12 @@ memchr = "2" netlink-packet-route = "0.19.0" nix = "0.28.0" oci-spec = "0.6.4" +once_cell = "1.19.0" path-absolutize = "3.1.1" path-clean = "1.0.1" prost = "0.12.3" prost-build = "0.12.3" +prost-reflect-build = "0.13.0" rand = "0.8.5" redb = "1.5.0" rtnetlink = "0.14.1" @@ -78,6 +80,10 @@ version = "0.4.6" version = "4.4.18" features = ["derive"] +[workspace.dependencies.prost-reflect] +version = "0.13.0" +features = ["derive"] + [workspace.dependencies.reqwest] version = "0.12.0" default-features = false diff --git a/crates/krata/Cargo.toml b/crates/krata/Cargo.toml index 2eb4a7e..5edf950 100644 --- a/crates/krata/Cargo.toml +++ b/crates/krata/Cargo.toml @@ -8,7 +8,9 @@ resolver = "2" anyhow = { workspace = true } libc = { workspace = true } log = { workspace = true } +once_cell = { workspace = true } prost = { workspace = true } +prost-reflect = { workspace = true } serde = { workspace = true } tokio = { workspace = true } tonic = { workspace = true } @@ -16,6 +18,8 @@ url = { workspace = true } [build-dependencies] tonic-build = { workspace = true } +prost-build = { workspace = true } +prost-reflect-build = { workspace = true } [lib] name = "krata" diff --git a/crates/krata/build.rs b/crates/krata/build.rs index 682ec54..cce5496 100644 --- a/crates/krata/build.rs +++ b/crates/krata/build.rs @@ -1,6 +1,14 @@ use std::io::Result; fn main() -> Result<()> { - tonic_build::configure().compile(&["proto/krata/control.proto"], &["proto/"])?; + let mut config = prost_build::Config::new(); + prost_reflect_build::Builder::new() + .descriptor_pool("crate::DESCRIPTOR_POOL") + .configure(&mut config, &["proto/krata/control.proto"], &["proto/"])?; + tonic_build::configure().compile_with_config( + config, + &["proto/krata/control.proto"], + &["proto/"], + )?; Ok(()) } diff --git a/crates/krata/src/lib.rs b/crates/krata/src/lib.rs index 3ba3599..d8545a3 100644 --- a/crates/krata/src/lib.rs +++ b/crates/krata/src/lib.rs @@ -1,3 +1,6 @@ +use once_cell::sync::Lazy; +use prost_reflect::DescriptorPool; + pub mod common; pub mod control; pub mod dial; @@ -5,3 +8,10 @@ pub mod launchcfg; #[cfg(target_os = "linux")] pub mod ethtool; + +pub static DESCRIPTOR_POOL: Lazy = Lazy::new(|| { + DescriptorPool::decode( + include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")).as_ref(), + ) + .unwrap() +}); diff --git a/crates/kratactl/Cargo.toml b/crates/kratactl/Cargo.toml index 010493e..c426401 100644 --- a/crates/kratactl/Cargo.toml +++ b/crates/kratactl/Cargo.toml @@ -14,7 +14,10 @@ ctrlc = { workspace = true, features = ["termination"] } env_logger = { workspace = true } krata = { path = "../krata" } log = { workspace = true } +prost-reflect = { workspace = true, features = ["serde"] } serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } signal-hook = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } diff --git a/crates/kratactl/src/cli/list.rs b/crates/kratactl/src/cli/list.rs index 8e0bcef..676196e 100644 --- a/crates/kratactl/src/cli/list.rs +++ b/crates/kratactl/src/cli/list.rs @@ -1,18 +1,37 @@ use anyhow::Result; -use clap::Parser; +use clap::{Parser, ValueEnum}; +use cli_tables::Table; use krata::{ - common::guest_image_spec::Image, + common::{guest_image_spec::Image, Guest}, control::{control_service_client::ControlServiceClient, ListGuestsRequest}, }; +use serde_json::Value; use tonic::{transport::Channel, Request}; -use crate::events::EventStream; +use crate::{ + events::EventStream, + format::{proto2dynamic, proto2kv}, +}; use super::pretty::guest_state_text; +#[derive(ValueEnum, Clone, Default, Debug, PartialEq, Eq)] +enum ListFormat { + #[default] + CliTable, + Json, + JsonPretty, + Jsonl, + Yaml, + KeyValue, +} + #[derive(Parser)] -pub struct ListCommand {} +pub struct ListCommand { + #[arg(short, long)] + format: ListFormat, +} impl ListCommand { pub async fn run( @@ -24,10 +43,49 @@ impl ListCommand { .list_guests(Request::new(ListGuestsRequest {})) .await? .into_inner(); - let mut table = cli_tables::Table::new(); + + match self.format { + ListFormat::CliTable => { + self.print_guest_table(response.guests)?; + } + + ListFormat::Json | ListFormat::JsonPretty | ListFormat::Yaml => { + let mut values = Vec::new(); + for guest in response.guests { + let message = proto2dynamic(guest)?; + values.push(serde_json::to_value(message)?); + } + let value = Value::Array(values); + let encoded = if self.format == ListFormat::JsonPretty { + serde_json::to_string_pretty(&value)? + } else if self.format == ListFormat::Yaml { + serde_yaml::to_string(&value)? + } else { + serde_json::to_string(&value)? + }; + println!("{}", encoded.trim()); + } + + ListFormat::Jsonl => { + for guest in response.guests { + let message = proto2dynamic(guest)?; + println!("{}", serde_json::to_string(&message)?); + } + } + + ListFormat::KeyValue => { + self.print_key_value(response.guests)?; + } + } + + Ok(()) + } + + fn print_guest_table(&self, guests: Vec) -> Result<()> { + let mut table = Table::new(); let header = vec!["name", "uuid", "state", "ipv4", "ipv6", "image"]; table.push_row(&header)?; - for guest in response.guests { + for guest in guests { let ipv4 = guest .network .as_ref() @@ -67,4 +125,18 @@ impl ListCommand { } Ok(()) } + + fn print_key_value(&self, guests: Vec) -> Result<()> { + for guest in guests { + let kvs = proto2kv(guest)?; + println!( + "{}", + kvs.iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect::>() + .join(" ") + ); + } + Ok(()) + } } diff --git a/crates/kratactl/src/format.rs b/crates/kratactl/src/format.rs new file mode 100644 index 0000000..5fcca42 --- /dev/null +++ b/crates/kratactl/src/format.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use anyhow::Result; +use prost_reflect::{DynamicMessage, ReflectMessage, Value}; + +pub fn proto2dynamic(proto: impl ReflectMessage) -> Result { + Ok(DynamicMessage::decode( + proto.descriptor(), + proto.encode_to_vec().as_slice(), + )?) +} + +pub fn proto2kv(proto: impl ReflectMessage) -> Result> { + let message = proto2dynamic(proto)?; + let mut map = HashMap::new(); + + fn crawl(prefix: &str, map: &mut HashMap, message: &DynamicMessage) { + for (field, value) in message.fields() { + let path = if prefix.is_empty() { + field.name().to_string() + } else { + format!("{}.{}", prefix, field.name()) + }; + match value { + Value::Message(child) => { + crawl(&path, map, child); + } + + Value::EnumNumber(number) => { + if let Some(e) = field.kind().as_enum() { + if let Some(value) = e.get_value(*number) { + map.insert(path, value.name().to_string()); + } + } + } + + Value::String(value) => { + map.insert(path, value.clone()); + } + + _ => { + map.insert(path, value.to_string()); + } + } + } + } + + crawl("", &mut map, &message); + + Ok(map) +} diff --git a/crates/kratactl/src/lib.rs b/crates/kratactl/src/lib.rs index 63e350b..7d37b0a 100644 --- a/crates/kratactl/src/lib.rs +++ b/crates/kratactl/src/lib.rs @@ -2,3 +2,4 @@ pub mod cli; pub mod client; pub mod console; pub mod events; +pub mod format;