Skip to main content

lean_ctx_patterns/
linkerd.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! linkerd (`linkerd check`) output compression.
4//!
5//! `linkerd check` prints a `√`/`×` line per check, grouped under section
6//! headers, ending with a `Status check results are …` line. Passing checks are
7//! noise once you know the total; failures (with their indented hint detail) and
8//! the final verdict are what matter. We keep failures + a pass/fail tally.
9
10use crate::ansi::strip_ansi;
11
12pub fn compress(command: &str, output: &str) -> Option<String> {
13    let sub = command
14        .trim()
15        .strip_prefix("linkerd")
16        .map_or("", str::trim_start)
17        .split_whitespace()
18        .next()
19        .unwrap_or("");
20    if sub != "check" {
21        return None;
22    }
23    Some(compress_check(output))
24}
25
26fn compress_check(output: &str) -> String {
27    let mut kept: Vec<String> = Vec::new();
28    let mut passed = 0usize;
29    let mut failed = 0usize;
30    let mut in_failure = false;
31
32    for raw in output.lines() {
33        let line = strip_ansi(raw);
34        let t = line.trim_end();
35        let probe = t.trim();
36        if probe.is_empty() {
37            in_failure = false;
38            continue;
39        }
40        // section underlines like "-----".
41        if probe.chars().all(|c| c == '-') {
42            continue;
43        }
44        if probe.starts_with('√') || probe.starts_with('✓') {
45            passed += 1;
46            in_failure = false;
47            continue;
48        }
49        if probe.starts_with('×') || probe.starts_with('✗') {
50            failed += 1;
51            in_failure = true;
52            kept.push(probe.to_string());
53            continue;
54        }
55        // indented hint/detail lines belong to the preceding failed check.
56        if in_failure && (t.starts_with(' ') || t.starts_with('\t')) {
57            kept.push(probe.to_string());
58            continue;
59        }
60        let pl = probe.to_ascii_lowercase();
61        if pl.starts_with("status check results") {
62            kept.push(probe.to_string());
63            in_failure = false;
64        }
65    }
66
67    let tally = format!("linkerd check: {passed} passed, {failed} failed");
68    if kept.is_empty() {
69        return tally;
70    }
71    format!("{tally}\n{}", kept.join("\n"))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    const CHECK: &str = "kubernetes-api\n--------------\n√ can initialize the client\n√ can query the Kubernetes API\n\nlinkerd-existence\n-----------------\n√ 'linkerd-config' config map exists\n× control plane pods are ready\n    some pods are not ready: linkerd-destination-abc\n    see https://linkerd.io/2/checks/#l5d-api-control-ready for hints\n\nStatus check results are ×\n";
79
80    #[test]
81    fn keeps_failures_and_verdict_drops_passing() {
82        let r = compress("linkerd check", CHECK).unwrap();
83        assert!(r.contains("× control plane pods are ready"), "{r}");
84        assert!(r.contains("some pods are not ready"), "keeps hint: {r}");
85        assert!(r.contains("Status check results are ×"), "{r}");
86        assert!(r.contains("3 passed, 1 failed"), "tally: {r}");
87        assert!(
88            !r.contains("can initialize the client"),
89            "drops passing: {r}"
90        );
91    }
92
93    #[test]
94    fn non_check_subcommand_passes_through() {
95        assert!(compress("linkerd viz stat deploy", "some table").is_none());
96    }
97}