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