kratactl/cli/zone/
list.rs1use anyhow::{anyhow, Result};
2use clap::{Parser, ValueEnum};
3use comfy_table::{presets::UTF8_FULL_CONDENSED, Cell, Color, Table};
4use krata::{
5 events::EventStream,
6 v1::{
7 common::Zone,
8 control::{
9 control_service_client::ControlServiceClient, ListZonesRequest, ResolveZoneIdRequest,
10 },
11 },
12};
13
14use crate::format::{kv2line, proto2dynamic, proto2kv, zone_simple_line, zone_state_text};
15use krata::v1::common::ZoneState;
16use krata::v1::control::GetZoneRequest;
17use serde_json::Value;
18use tonic::{transport::Channel, Request};
19
20#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
21enum ZoneListFormat {
22 Table,
23 Json,
24 JsonPretty,
25 Jsonl,
26 Yaml,
27 KeyValue,
28 Simple,
29}
30
31#[derive(Parser)]
32#[command(about = "List zone information")]
33pub struct ZoneListCommand {
34 #[arg(short, long, default_value = "table", help = "Output format")]
35 format: ZoneListFormat,
36 #[arg(help = "Limit to a single zone, either the name or the uuid")]
37 zone: Option<String>,
38}
39
40impl ZoneListCommand {
41 pub async fn run(
42 self,
43 mut client: ControlServiceClient<Channel>,
44 _events: EventStream,
45 ) -> Result<()> {
46 let mut zones = if let Some(ref zone) = self.zone {
47 let reply = client
48 .resolve_zone_id(Request::new(ResolveZoneIdRequest { name: zone.clone() }))
49 .await?
50 .into_inner();
51 if !reply.zone_id.is_empty() {
52 let reply = client
53 .get_zone(Request::new(GetZoneRequest {
54 zone_id: reply.zone_id,
55 }))
56 .await?
57 .into_inner();
58 if let Some(zone) = reply.zone {
59 vec![zone]
60 } else {
61 return Err(anyhow!("unable to resolve zone '{}'", zone));
62 }
63 } else {
64 return Err(anyhow!("unable to resolve zone '{}'", zone));
65 }
66 } else {
67 client
68 .list_zones(Request::new(ListZonesRequest {}))
69 .await?
70 .into_inner()
71 .zones
72 };
73
74 zones.sort_by(|a, b| {
75 a.spec
76 .as_ref()
77 .map(|x| x.name.as_str())
78 .unwrap_or("")
79 .cmp(b.spec.as_ref().map(|x| x.name.as_str()).unwrap_or(""))
80 });
81
82 match self.format {
83 ZoneListFormat::Table => {
84 self.print_zone_table(zones)?;
85 }
86
87 ZoneListFormat::Simple => {
88 for zone in zones {
89 println!("{}", zone_simple_line(&zone));
90 }
91 }
92
93 ZoneListFormat::Json | ZoneListFormat::JsonPretty | ZoneListFormat::Yaml => {
94 let mut values = Vec::new();
95 for zone in zones {
96 let message = proto2dynamic(zone)?;
97 values.push(serde_json::to_value(message)?);
98 }
99 let value = Value::Array(values);
100 let encoded = if self.format == ZoneListFormat::JsonPretty {
101 serde_json::to_string_pretty(&value)?
102 } else if self.format == ZoneListFormat::Yaml {
103 serde_yaml::to_string(&value)?
104 } else {
105 serde_json::to_string(&value)?
106 };
107 println!("{}", encoded.trim());
108 }
109
110 ZoneListFormat::Jsonl => {
111 for zone in zones {
112 let message = proto2dynamic(zone)?;
113 println!("{}", serde_json::to_string(&message)?);
114 }
115 }
116
117 ZoneListFormat::KeyValue => {
118 self.print_key_value(zones)?;
119 }
120 }
121
122 Ok(())
123 }
124
125 fn print_zone_table(&self, zones: Vec<Zone>) -> Result<()> {
126 let mut table = Table::new();
127 table.load_preset(UTF8_FULL_CONDENSED);
128 table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
129 table.set_header(vec!["name", "uuid", "state", "ipv4", "ipv6"]);
130 for zone in zones {
131 let ipv4 = zone
132 .status
133 .as_ref()
134 .and_then(|x| x.network_status.as_ref())
135 .map(|x| x.zone_ipv4.as_str())
136 .unwrap_or("n/a");
137 let ipv6 = zone
138 .status
139 .as_ref()
140 .and_then(|x| x.network_status.as_ref())
141 .map(|x| x.zone_ipv6.as_str())
142 .unwrap_or("n/a");
143 let Some(spec) = zone.spec else {
144 continue;
145 };
146 let state = zone.status.as_ref().cloned().unwrap_or_default().state();
147 let status_text = zone_state_text(state);
148
149 let status_color = match state {
150 ZoneState::Destroyed | ZoneState::Failed => Color::Red,
151 ZoneState::Destroying | ZoneState::Exited | ZoneState::Creating => Color::Yellow,
152 ZoneState::Created => Color::Green,
153 _ => Color::Reset,
154 };
155
156 table.add_row(vec![
157 Cell::new(spec.name),
158 Cell::new(zone.id),
159 Cell::new(status_text).fg(status_color),
160 Cell::new(ipv4.to_string()),
161 Cell::new(ipv6.to_string()),
162 ]);
163 }
164 if table.is_empty() {
165 if self.zone.is_none() {
166 println!("no zones have been launched");
167 }
168 } else {
169 println!("{}", table);
170 }
171 Ok(())
172 }
173
174 fn print_key_value(&self, zones: Vec<Zone>) -> Result<()> {
175 for zone in zones {
176 let kvs = proto2kv(zone)?;
177 println!("{}", kv2line(kvs),);
178 }
179 Ok(())
180 }
181}