lean_ctx_patterns/
argocd.rs1use crate::ansi::strip_ansi;
11
12const KEYS: &[&str] = &[
13 "Name:",
14 "URL:",
15 "Project:",
16 "Sync Status:",
17 "Health Status:",
18 "Operation:",
19 "Phase:",
20 "Message:",
21];
22
23pub fn compress(command: &str, output: &str) -> Option<String> {
24 let sub = command
25 .trim()
26 .strip_prefix("argocd")
27 .map_or("", str::trim_start);
28 if sub.starts_with("app get") || sub.starts_with("app sync") || sub.starts_with("app wait") {
29 return Some(compress_app(output));
30 }
31 if sub.starts_with("app list") {
32 return Some(compress_table(output, "argocd app list"));
33 }
34 None
35}
36
37fn compress_app(output: &str) -> String {
38 let mut header: Vec<String> = Vec::new();
39 let mut table: Vec<String> = Vec::new();
40 let mut in_table = false;
41
42 for raw in output.lines() {
43 let line = strip_ansi(raw);
44 let t = line.trim();
45 if t.is_empty() {
46 continue;
47 }
48 if is_table_header(t) {
49 in_table = true;
50 table.push(t.to_string());
51 continue;
52 }
53 if in_table {
54 table.push(t.to_string());
55 continue;
56 }
57 if KEYS.iter().any(|k| t.starts_with(k)) || t.to_ascii_lowercase().contains("error") {
58 header.push(t.to_string());
59 }
60 }
61
62 let mut parts = header;
63 if table.len() > 1 {
64 parts.push(filter_rows(&table));
65 }
66 if parts.is_empty() {
67 return "argocd: ok".to_string();
68 }
69 parts.join("\n")
70}
71
72fn compress_table(output: &str, label: &str) -> String {
73 let rows: Vec<&str> = output.lines().map(str::trim_end).collect();
74 let table: Vec<String> = rows
75 .iter()
76 .map(|l| strip_ansi(l).trim().to_string())
77 .filter(|l| !l.is_empty())
78 .collect();
79 if table.is_empty() {
80 return format!("{label}: ok");
81 }
82 filter_rows(&table)
83}
84
85fn filter_rows(table: &[String]) -> String {
87 let header = &table[0];
88 let mut kept: Vec<String> = vec![header.clone()];
89 let mut healthy = 0usize;
90 for row in &table[1..] {
91 if row.contains("Synced") && row.contains("Healthy") {
92 healthy += 1;
93 } else {
94 kept.push(row.clone());
95 }
96 }
97 let total = table.len() - 1;
98 let mut s = kept.join("\n");
99 if healthy > 0 {
100 s.push_str(&format!(
101 "\n({healthy}/{total} resources Synced+Healthy, hidden)"
102 ));
103 }
104 s
105}
106
107fn is_table_header(t: &str) -> bool {
108 let u = t.to_ascii_uppercase();
109 (u.starts_with("GROUP") || u.starts_with("NAME") || u.starts_with("TIMESTAMP"))
110 && u.contains("STATUS")
111 && u.contains("HEALTH")
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 const GET: &str = "Name: argocd/myapp\nProject: default\nURL: https://argocd.example.com/applications/myapp\nSync Status: Synced to HEAD (abc1234)\nHealth Status: Healthy\n\nGROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE\n Service myns mysvc Synced Healthy service/mysvc created\napps Deployment myns mydeploy OutOfSync Progressing waiting for rollout\n";
119
120 #[test]
121 fn keeps_status_and_unhealthy_rows() {
122 let r = compress("argocd app get myapp", GET).unwrap();
123 assert!(r.contains("Sync Status:"), "{r}");
124 assert!(r.contains("Health Status: Healthy"), "{r}");
125 assert!(r.contains("mydeploy"), "keeps unhealthy row: {r}");
126 assert!(r.contains("OutOfSync"), "{r}");
127 assert!(!r.contains("mysvc"), "drops synced+healthy row: {r}");
128 assert!(r.contains("1/2 resources"), "tally: {r}");
129 }
130
131 #[test]
132 fn app_list_keeps_header_and_unhealthy() {
133 let list = "NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY\napp-a in-cl ns-a default Synced Healthy Automated\napp-b in-cl ns-b default OutOfSync Degraded Automated";
134 let r = compress("argocd app list", list).unwrap();
135 assert!(r.contains("app-b"), "keeps unhealthy: {r}");
136 assert!(!r.contains("app-a"), "drops healthy: {r}");
137 }
138
139 #[test]
140 fn non_app_subcommand_none() {
141 assert!(compress("argocd version", "v2.9.0").is_none());
142 }
143}