kratactl: implement output formats

This commit is contained in:
Alex Zenla 2024-03-23 04:07:48 +00:00
parent 3b5e3a077a
commit 6c0e14da6a
No known key found for this signature in database
GPG Key ID: 067B238899B51269
8 changed files with 162 additions and 7 deletions

View File

@ -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

View File

@ -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"

View File

@ -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(())
}

View File

@ -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<DescriptorPool> = Lazy::new(|| {
DescriptorPool::decode(
include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin")).as_ref(),
)
.unwrap()
});

View File

@ -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 }

View File

@ -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<Guest>) -> 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<Guest>) -> Result<()> {
for guest in guests {
let kvs = proto2kv(guest)?;
println!(
"{}",
kvs.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(" ")
);
}
Ok(())
}
}

View File

@ -0,0 +1,51 @@
use std::collections::HashMap;
use anyhow::Result;
use prost_reflect::{DynamicMessage, ReflectMessage, Value};
pub fn proto2dynamic(proto: impl ReflectMessage) -> Result<DynamicMessage> {
Ok(DynamicMessage::decode(
proto.descriptor(),
proto.encode_to_vec().as_slice(),
)?)
}
pub fn proto2kv(proto: impl ReflectMessage) -> Result<HashMap<String, String>> {
let message = proto2dynamic(proto)?;
let mut map = HashMap::new();
fn crawl(prefix: &str, map: &mut HashMap<String, String>, 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)
}

View File

@ -2,3 +2,4 @@ pub mod cli;
pub mod client;
pub mod console;
pub mod events;
pub mod format;