kratactl/cli/zone/
list.rs

1use 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}