Skip to main content

lean_ctx/core/patterns/
terraform.rs

1macro_rules! static_regex {
2    ($pattern:expr) => {{
3        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4        RE.get_or_init(|| {
5            regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6        })
7    }};
8}
9
10fn plan_summary_re() -> &'static regex::Regex {
11    static_regex!(r"Plan:\s*(\d+)\s+to add,\s*(\d+)\s+to change,\s*(\d+)\s+to destroy")
12}
13
14fn apply_summary_re() -> &'static regex::Regex {
15    static_regex!(
16        r"Apply complete!\s*Resources:\s*(\d+)\s+added,\s*(\d+)\s+changed,\s*(\d+)\s+destroyed"
17    )
18}
19
20fn installed_provider_re() -> &'static regex::Regex {
21    static_regex!(r"-\s*Installed\s+([^\s]+)\s+v([0-9][^\s]*)")
22}
23
24fn provider_version_re() -> &'static regex::Regex {
25    static_regex!(r"\*\s*provider\[([^\]]+)\]\s+([0-9][^\s]*)")
26}
27
28fn is_provider_init_noise(line: &str) -> bool {
29    let t = line.trim_start();
30    let tl = t.to_ascii_lowercase();
31    tl.contains("initializing provider plugins")
32        || tl.contains("initializing the backend")
33        || tl.contains("finding ")
34            && (tl.contains("versions matching") || tl.contains("version of"))
35        || tl.starts_with("- finding ")
36        || tl.starts_with("- installing ")
37        || tl.contains("terraform init") && tl.contains("upgrade")
38        || tl.starts_with("╷")
39        || tl.starts_with("╵")
40        || tl.starts_with("│")
41}
42
43pub fn compress(command: &str, output: &str) -> Option<String> {
44    let c = command.trim();
45    let prefix = if c == "terraform" || c.starts_with("terraform ") {
46        "terraform"
47    } else if c == "tofu" || c.starts_with("tofu ") {
48        "tofu"
49    } else {
50        return None;
51    };
52    let sub = c.strip_prefix(prefix).map_or("", str::trim_start);
53    let sub_cmd = sub.split_whitespace().next().unwrap_or("");
54
55    match sub_cmd {
56        "plan" => Some(compress_plan(output)),
57        "apply" => Some(compress_apply(output)),
58        "init" => Some(compress_init(output)),
59        "validate" => Some(compress_validate(output)),
60        _ => Some(compress_generic(output)),
61    }
62}
63
64fn compress_plan(output: &str) -> String {
65    let mut kept = Vec::new();
66
67    for line in output.lines() {
68        if is_provider_init_noise(line) {
69            continue;
70        }
71        let tl = line.trim_start();
72        if tl.starts_with("- Installed ") || tl.starts_with("- Installing ") {
73            continue;
74        }
75
76        if let Some(caps) = plan_summary_re().captures(line) {
77            let add = caps.get(1).map_or("0", |m| m.as_str());
78            let chg = caps.get(2).map_or("0", |m| m.as_str());
79            let des = caps.get(3).map_or("0", |m| m.as_str());
80            kept.push(format!("+ {add} added, ~ {chg} changed, - {des} destroyed"));
81            continue;
82        }
83
84        let l = line.to_ascii_lowercase();
85        if l.contains("no changes.") || l.contains("infrastructure matches the configuration") {
86            kept.push("No changes.".to_string());
87            continue;
88        }
89
90        let is_diag = tl.contains('╷')
91            || tl.contains('│')
92            || tl.contains('╵')
93            || l.contains("error:")
94            || (l.contains("error ")
95                && (l.contains("terraform") || l.contains("plan") || l.contains("provider")))
96            || l.contains("warning:")
97            || l.contains("warning ");
98        if is_diag {
99            kept.push(line.trim().to_string());
100        }
101    }
102
103    if kept.is_empty() {
104        "terraform plan (no summary parsed)".to_string()
105    } else {
106        kept.join("\n")
107    }
108}
109
110fn compress_apply(output: &str) -> String {
111    let mut results = Vec::new();
112    let mut errors = Vec::new();
113
114    for line in output.lines() {
115        if is_provider_init_noise(line) {
116            continue;
117        }
118        let tl = line.trim();
119        if tl.is_empty() {
120            continue;
121        }
122
123        if let Some(caps) = apply_summary_re().captures(line) {
124            let a = caps.get(1).map_or("0", |m| m.as_str());
125            let c = caps.get(2).map_or("0", |m| m.as_str());
126            let d = caps.get(3).map_or("0", |m| m.as_str());
127            results.push(format!(
128                "Apply complete: +{a} added, ~{c} changed, -{d} destroyed"
129            ));
130            continue;
131        }
132
133        let ll = tl.to_ascii_lowercase();
134        if ll.contains("error")
135            && (ll.contains("apply") || ll.contains("terraform") || tl.contains('╷'))
136        {
137            errors.push(tl.to_string());
138        } else if ll.starts_with("creation complete")
139            || ll.starts_with("modification complete")
140            || ll.starts_with("destruction complete")
141            || ll.starts_with("destroy complete")
142        {
143            results.push(tl.to_string());
144        }
145    }
146
147    let mut out = Vec::new();
148    if !results.is_empty() {
149        out.push(results.join("\n"));
150    }
151    if !errors.is_empty() {
152        out.push(format!("errors:\n{}", errors.join("\n")));
153    }
154    if out.is_empty() {
155        "terraform apply (no summary parsed)".to_string()
156    } else {
157        out.join("\n\n")
158    }
159}
160
161fn compress_init(output: &str) -> String {
162    let mut providers: Vec<String> = Vec::new();
163    let mut success = false;
164
165    for line in output.lines() {
166        let tl = line.trim();
167        if tl.is_empty() {
168            continue;
169        }
170        let ll = tl.to_ascii_lowercase();
171        if ll.contains("terraform has been successfully initialized")
172            || ll.contains("initialization complete")
173        {
174            success = true;
175        }
176        if let Some(caps) = installed_provider_re().captures(tl) {
177            let name = caps.get(1).map_or("?", |m| m.as_str());
178            let ver = caps.get(2).map_or("?", |m| m.as_str());
179            providers.push(format!("{name} v{ver}"));
180            continue;
181        }
182        if let Some(caps) = provider_version_re().captures(tl) {
183            let reg = caps.get(1).map_or("?", |m| m.as_str());
184            let ver = caps.get(2).map_or("?", |m| m.as_str());
185            providers.push(format!("{reg} {ver}"));
186        }
187    }
188
189    let status = if success {
190        "Terraform initialized"
191    } else {
192        "terraform init"
193    };
194
195    if providers.is_empty() {
196        status.to_string()
197    } else {
198        format!("{status}\n{}", providers.join(", "))
199    }
200}
201
202fn compress_validate(output: &str) -> String {
203    let mut errs = Vec::new();
204    for line in output.lines() {
205        let tl = line.trim();
206        if tl.is_empty() {
207            continue;
208        }
209        let ll = tl.to_ascii_lowercase();
210        if ll.contains("success!") && ll.contains("configuration is valid") {
211            return "Success".to_string();
212        }
213        if ll.contains("error") || tl.starts_with('╷') || tl.starts_with('│') {
214            errs.push(tl.to_string());
215        }
216    }
217    if errs.is_empty() {
218        "Success".to_string()
219    } else {
220        errs.join("\n")
221    }
222}
223
224fn compress_generic(output: &str) -> String {
225    let mut lines: Vec<String> = output
226        .lines()
227        .filter(|l| !is_provider_init_noise(l))
228        .map(|l| l.trim().to_string())
229        .filter(|l| !l.is_empty())
230        .collect();
231    if lines.len() > 40 {
232        let n = lines.len();
233        lines = lines.split_off(n - 25);
234        format!("... (truncated)\n{}", lines.join("\n"))
235    } else {
236        lines.join("\n")
237    }
238}