1use std::collections::HashMap;
7
8use bollard::Docker;
9use bollard::plugin::EndpointSettings;
10use bollard::plugin::PortBinding;
11use bollard::query_parameters::ListContainersOptionsBuilder;
12use color_eyre::eyre::Result;
13use comfy_table::Attribute;
14use comfy_table::Color;
15use comfy_table::Table;
16
17pub async fn run(use_color: bool) -> Result<()> {
30 let mut table = Table::new();
31 let mut rows = Vec::new();
32
33 let headers: Vec<comfy_table::Cell> = if use_color {
34 vec![
35 comfy_table::Cell::new("Name")
36 .fg(Color::Cyan)
37 .add_attribute(Attribute::Bold),
38 comfy_table::Cell::new("Status")
39 .fg(Color::Cyan)
40 .add_attribute(Attribute::Bold),
41 comfy_table::Cell::new("IP")
42 .fg(Color::Cyan)
43 .add_attribute(Attribute::Bold),
44 comfy_table::Cell::new("Binds")
45 .fg(Color::Cyan)
46 .add_attribute(Attribute::Bold),
47 ]
48 } else {
49 vec!["Name".into(), "Status".into(), "IP".into(), "Binds".into()]
50 };
51 table.set_header(headers);
52
53 let docker = Docker::connect_with_local_defaults()?;
54 let opt = Some(ListContainersOptionsBuilder::new().all(true).build());
55
56 for cont in docker.list_containers(opt).await? {
57 let Some(id) = cont.id else { continue };
58 let status = cont.status.unwrap_or_default();
59 if is_exit(&status) {
60 continue;
61 }
62
63 let name = format_name(cont.names);
64
65 let Some(network) = docker.inspect_container(&id, None).await?.network_settings else {
66 continue;
67 };
68
69 let ips = format_ips(network.networks);
70 let binds = format_binds(network.ports);
71
72 rows.push([name, status, ips, binds]);
73 }
74
75 rows.sort_by_key(|r| r[0].clone());
76 rows.sort_by_key(|r| r[1].clone());
77 rows.sort_by_key(|r| r[3].clone());
78
79 if use_color {
80 for row in &rows {
81 let status_cell = if row[1].contains("running") || row[1].contains("Up") {
82 comfy_table::Cell::new(&row[1]).fg(Color::Green)
83 } else if row[1].contains("Exited") || row[1].contains("Dead") {
84 comfy_table::Cell::new(&row[1]).fg(Color::Red)
85 } else {
86 comfy_table::Cell::new(&row[1])
87 };
88 table.add_row(vec![
89 comfy_table::Cell::new(&row[0]),
90 status_cell,
91 comfy_table::Cell::new(&row[2]),
92 comfy_table::Cell::new(&row[3]),
93 ]);
94 }
95 } else {
96 table.add_rows(rows);
97 }
98
99 println!("{table}");
100 Ok(())
101}
102
103fn is_exit(status: impl AsRef<str>) -> bool {
105 status.as_ref().contains("Exited (0)")
106}
107
108fn format_name(names: Option<Vec<String>>) -> String {
110 names
111 .unwrap_or_default()
112 .into_iter()
113 .next()
114 .unwrap_or_default()
115}
116
117fn format_ips(networks: Option<HashMap<String, EndpointSettings>>) -> String {
119 let Some(networks) = networks else {
120 return String::new();
121 };
122 networks
123 .values()
124 .filter_map(|nw| nw.ip_address.clone())
125 .collect::<Vec<_>>()
126 .join("\n")
127}
128
129fn format_binds(ports: Option<HashMap<String, Option<Vec<PortBinding>>>>) -> String {
131 let Some(ports) = ports else {
132 return String::new();
133 };
134 ports
135 .iter()
136 .filter_map(|(port, binds)| {
137 let binds = binds.as_ref()?;
138 let proto = port.split('/').nth(1).unwrap_or("");
139 let addrs = binds
140 .iter()
141 .filter_map(|b| {
142 let ip = b.host_ip.as_ref()?;
143 let port = b.host_port.as_deref().unwrap_or("");
144 Some(format!("{ip}:{port}"))
145 })
146 .collect::<Vec<_>>()
147 .join(", ");
148 Some(format!("{proto}: {addrs}"))
149 })
150 .collect::<Vec<_>>()
151 .join("\n")
152}
153
154#[cfg(test)]
155mod tests {
156 use std::collections::HashMap;
157
158 use bollard::models::EndpointSettings;
159 use bollard::models::PortBinding;
160
161 use super::*;
162
163 fn make_networks(ips: &[&str]) -> Option<HashMap<String, EndpointSettings>> {
164 Some(
165 ips.iter()
166 .enumerate()
167 .map(|(i, ip)| {
168 (
169 format!("net{i}"),
170 EndpointSettings {
171 ip_address: Some(ip.to_string()),
172 ..Default::default()
173 },
174 )
175 })
176 .collect(),
177 )
178 }
179
180 fn make_ports(
181 bindings: &[(&str, &[(&str, &str)])],
182 ) -> Option<HashMap<String, Option<Vec<PortBinding>>>> {
183 Some(
184 bindings
185 .iter()
186 .map(|(port, binds)| {
187 let binds = binds
188 .iter()
189 .map(|(ip, p)| PortBinding {
190 host_ip: Some(ip.to_string()),
191 host_port: Some(p.to_string()),
192 })
193 .collect();
194 (port.to_string(), Some(binds))
195 })
196 .collect(),
197 )
198 }
199
200 #[test]
201 fn format_ips_none() {
202 assert_eq!(format_ips(None), "");
203 }
204
205 #[test]
206 fn format_ips_single() {
207 assert_eq!(format_ips(make_networks(&["172.19.0.2"])), "172.19.0.2");
208 }
209
210 #[test]
211 fn format_ips_multiple() {
212 let result = format_ips(make_networks(&["172.19.0.2", "10.0.0.1"]));
213 let mut lines: Vec<&str> = result.lines().collect();
214 lines.sort();
215 assert_eq!(lines, vec!["10.0.0.1", "172.19.0.2"]);
216 }
217
218 #[test]
219 fn format_binds_none() {
220 assert_eq!(format_binds(None), "");
221 }
222
223 #[test]
224 fn format_binds_single() {
225 let ports = make_ports(&[("5432/tcp", &[("0.0.0.0", "5432")])]);
226 assert_eq!(format_binds(ports), "tcp: 0.0.0.0:5432");
227 }
228
229 #[test]
230 fn format_binds_multiple_ips() {
231 let ports = make_ports(&[("5432/tcp", &[("0.0.0.0", "5432"), ("::", "5432")])]);
232 assert_eq!(format_binds(ports), "tcp: 0.0.0.0:5432, :::5432");
233 }
234
235 #[test]
236 fn format_binds_no_proto() {
237 let ports = make_ports(&[("5432", &[("0.0.0.0", "5432")])]);
238 assert_eq!(format_binds(ports), ": 0.0.0.0:5432");
239 }
240
241 #[test]
242 fn format_binds_null_binding() {
243 let mut map = HashMap::new();
244 map.insert("5432/tcp".to_string(), None);
245 assert_eq!(format_binds(Some(map)), "");
246 }
247}