1use crate::agent::ui::colors::icons;
7use crate::agent::ui::response::brand;
8use std::io::{self, Write};
9
10const BOX_WIDTH: usize = 72;
12
13pub struct KubelintDisplay;
15
16impl KubelintDisplay {
17 pub fn print_result(json_result: &str) {
19 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_result) {
20 Self::print_formatted(&parsed);
21 } else {
22 println!("{}", json_result);
24 }
25 }
26
27 fn print_formatted(result: &serde_json::Value) {
29 let stdout = io::stdout();
30 let mut handle = stdout.lock();
31
32 let source = result["source"].as_str().unwrap_or("kubernetes manifests");
34
35 let _ = writeln!(handle);
37 let _ = writeln!(
38 handle,
39 "{}{}╭─ {} Kubelint {}{}╮{}",
40 brand::PURPLE,
41 brand::BOLD,
42 icons::KUBERNETES,
43 "─".repeat(BOX_WIDTH - 16),
44 brand::DIM,
45 brand::RESET
46 );
47
48 let _ = writeln!(
50 handle,
51 "{}│ {}{}{}{}",
52 brand::DIM,
53 brand::CYAN,
54 source,
55 " ".repeat((BOX_WIDTH - 4 - source.len()).max(0)),
56 brand::RESET
57 );
58
59 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
61
62 if let Some(context) = result["decision_context"].as_str() {
64 let context_color = if context.contains("CRITICAL") {
65 brand::CORAL
66 } else if context.contains("High") || context.contains("high") {
67 brand::PEACH
68 } else if context.contains("Good") || context.contains("No issues") {
69 brand::SUCCESS
70 } else {
71 brand::PEACH
72 };
73
74 let display_context = if context.len() > BOX_WIDTH - 6 {
76 &context[..BOX_WIDTH - 9]
77 } else {
78 context
79 };
80
81 let _ = writeln!(
82 handle,
83 "{}│ {}{}{}{}",
84 brand::DIM,
85 context_color,
86 display_context,
87 " ".repeat((BOX_WIDTH - 4 - display_context.len()).max(0)),
88 brand::RESET
89 );
90 }
91
92 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
94
95 if let Some(summary) = result.get("summary") {
97 let total = summary["total_issues"].as_u64().unwrap_or(0);
98
99 if total == 0 {
100 let _ = writeln!(
101 handle,
102 "{}│ {}{} All checks passed! No issues found.{}{}",
103 brand::DIM,
104 brand::SUCCESS,
105 icons::SUCCESS,
106 " ".repeat(BOX_WIDTH - 42),
107 brand::RESET
108 );
109
110 let objects = summary["objects_analyzed"].as_u64().unwrap_or(0);
112 let checks = summary["checks_run"].as_u64().unwrap_or(0);
113 let stats = format!("{} objects analyzed • {} checks run", objects, checks);
114 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
115 let _ = writeln!(
116 handle,
117 "{}│ {}{}{}{}",
118 brand::DIM,
119 brand::DIM,
120 stats,
121 " ".repeat((BOX_WIDTH - 4 - stats.len()).max(0)),
122 brand::RESET
123 );
124 } else {
125 if let Some(by_priority) = summary.get("by_priority") {
127 let critical = by_priority["critical"].as_u64().unwrap_or(0);
128 let high = by_priority["high"].as_u64().unwrap_or(0);
129 let medium = by_priority["medium"].as_u64().unwrap_or(0);
130 let low = by_priority["low"].as_u64().unwrap_or(0);
131
132 let mut counts = String::new();
133 if critical > 0 {
134 counts.push_str(&format!("{} {} critical ", icons::CRITICAL, critical));
135 }
136 if high > 0 {
137 counts.push_str(&format!("{} {} high ", icons::HIGH, high));
138 }
139 if medium > 0 {
140 counts.push_str(&format!("{} {} medium ", icons::MEDIUM, medium));
141 }
142 if low > 0 {
143 counts.push_str(&format!("{} {} low", icons::LOW, low));
144 }
145
146 let padding = if counts.len() < BOX_WIDTH - 4 {
147 (BOX_WIDTH - 4 - counts.chars().count()).max(0)
148 } else {
149 0
150 };
151 let _ = writeln!(
152 handle,
153 "{}│ {}{}{}",
154 brand::DIM,
155 counts,
156 " ".repeat(padding),
157 brand::RESET
158 );
159 }
160 }
161 }
162
163 if let Some(quick_fixes) = result.get("quick_fixes").and_then(|f| f.as_array())
165 && !quick_fixes.is_empty()
166 {
167 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
168 let _ = writeln!(
169 handle,
170 "{}│ {}{} Quick Fixes:{}{}",
171 brand::DIM,
172 brand::PURPLE,
173 icons::FIX,
174 " ".repeat(BOX_WIDTH - 18),
175 brand::RESET
176 );
177
178 for fix in quick_fixes.iter().take(5) {
179 if let Some(fix_str) = fix.as_str() {
180 let (issue, remediation) = if let Some(pos) = fix_str.find(" - ") {
182 (&fix_str[..pos], &fix_str[pos + 3..])
183 } else {
184 (fix_str, "")
185 };
186
187 let issue_display = if issue.len() > BOX_WIDTH - 10 {
188 format!("{}...", &issue[..BOX_WIDTH - 13])
189 } else {
190 issue.to_string()
191 };
192
193 let _ = writeln!(
194 handle,
195 "{}│ {}→ {}{}{}{}",
196 brand::DIM,
197 brand::CYAN,
198 issue_display,
199 " ".repeat((BOX_WIDTH - 8 - issue_display.len()).max(0)),
200 brand::RESET,
201 brand::RESET
202 );
203
204 if !remediation.is_empty() {
205 let rem_display = if remediation.len() > BOX_WIDTH - 10 {
206 format!("{}...", &remediation[..BOX_WIDTH - 13])
207 } else {
208 remediation.to_string()
209 };
210 let _ = writeln!(
211 handle,
212 "{}│ {}{}{}{}",
213 brand::DIM,
214 brand::DIM,
215 rem_display,
216 " ".repeat((BOX_WIDTH - 8 - rem_display.len()).max(0)),
217 brand::RESET
218 );
219 }
220 }
221 }
222 }
223
224 Self::print_priority_section(
226 &mut handle,
227 result,
228 "critical",
229 "Critical Issues",
230 brand::CORAL,
231 );
232 Self::print_priority_section(&mut handle, result, "high", "High Priority", brand::PEACH);
233
234 let medium_count = result["action_plan"]["medium"]
236 .as_array()
237 .map(|a| a.len())
238 .unwrap_or(0);
239 let low_count = result["action_plan"]["low"]
240 .as_array()
241 .map(|a| a.len())
242 .unwrap_or(0);
243 let other_count = medium_count + low_count;
244
245 if other_count > 0 {
246 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
247 let msg = format!(
248 "{} {} priority issue{} (use --verbose to see all)",
249 other_count,
250 if medium_count > 0 {
251 "medium/low"
252 } else {
253 "low"
254 },
255 if other_count == 1 { "" } else { "s" }
256 );
257 let _ = writeln!(
258 handle,
259 "{}│ {}{}{}{}",
260 brand::DIM,
261 brand::DIM,
262 msg,
263 " ".repeat((BOX_WIDTH - 4 - msg.len()).max(0)),
264 brand::RESET
265 );
266 }
267
268 let _ = writeln!(
270 handle,
271 "{}╰{}╯{}",
272 brand::DIM,
273 "─".repeat(BOX_WIDTH - 2),
274 brand::RESET
275 );
276 let _ = writeln!(handle);
277
278 let _ = handle.flush();
279 }
280
281 fn print_priority_section(
283 handle: &mut io::StdoutLock,
284 result: &serde_json::Value,
285 priority: &str,
286 title: &str,
287 color: &str,
288 ) {
289 if let Some(issues) = result["action_plan"][priority].as_array() {
290 if issues.is_empty() {
291 return;
292 }
293
294 let _ = writeln!(handle, "{}│{}", brand::DIM, " ".repeat(BOX_WIDTH - 1));
295 let _ = writeln!(
296 handle,
297 "{}│ {}{}:{}{}",
298 brand::DIM,
299 color,
300 title,
301 " ".repeat((BOX_WIDTH - 4 - title.len() - 1).max(0)),
302 brand::RESET
303 );
304
305 for issue in issues.iter().take(5) {
306 let code = issue["check"].as_str().unwrap_or("???");
307 let line = issue["line"].as_u64().unwrap_or(0);
308 let message = issue["message"].as_str().unwrap_or("");
309 let category = issue["category"].as_str().unwrap_or("");
310
311 let badge = Self::get_category_badge(category);
313
314 let header = format!("Line {} • {} {}", line, code, badge);
316 let _ = writeln!(
317 handle,
318 "{}│ {}{}{}{}",
319 brand::DIM,
320 brand::CYAN,
321 header,
322 " ".repeat((BOX_WIDTH - 6 - header.chars().count()).max(0)),
323 brand::RESET
324 );
325
326 let msg_display = if message.len() > BOX_WIDTH - 8 {
328 format!("{}...", &message[..BOX_WIDTH - 11])
329 } else {
330 message.to_string()
331 };
332 let _ = writeln!(
333 handle,
334 "{}│ {}{}{}",
335 brand::DIM,
336 msg_display,
337 " ".repeat((BOX_WIDTH - 6 - msg_display.len()).max(0)),
338 brand::RESET
339 );
340
341 if let Some(remediation) = issue["remediation"].as_str() {
343 let rem_display = if remediation.len() > BOX_WIDTH - 12 {
344 format!("{}...", &remediation[..BOX_WIDTH - 15])
345 } else {
346 remediation.to_string()
347 };
348 let _ = writeln!(
349 handle,
350 "{}│ {}→ {}{}{}",
351 brand::DIM,
352 brand::CYAN,
353 rem_display,
354 " ".repeat((BOX_WIDTH - 8 - rem_display.len()).max(0)),
355 brand::RESET
356 );
357 }
358 }
359
360 if issues.len() > 5 {
361 let more_msg = format!("... and {} more", issues.len() - 5);
362 let _ = writeln!(
363 handle,
364 "{}│ {}{}{}{}",
365 brand::DIM,
366 brand::DIM,
367 more_msg,
368 " ".repeat((BOX_WIDTH - 6 - more_msg.len()).max(0)),
369 brand::RESET
370 );
371 }
372 }
373 }
374
375 fn get_category_badge(category: &str) -> String {
377 match category {
378 "security" => format!("{}[SEC]{}", brand::CORAL, brand::RESET),
379 "rbac" => format!("{}[RBAC]{}", brand::CORAL, brand::RESET),
380 "best-practice" => format!("{}[BP]{}", brand::CYAN, brand::RESET),
381 "validation" => format!("{}[VAL]{}", brand::PEACH, brand::RESET),
382 "ports" => format!("{}[PORT]{}", brand::PEACH, brand::RESET),
383 "disruption-budget" => format!("{}[PDB]{}", brand::DIM, brand::RESET),
384 "autoscaling" => format!("{}[HPA]{}", brand::DIM, brand::RESET),
385 "deprecated-api" => format!("{}[DEP]{}", brand::PEACH, brand::RESET),
386 "service" => format!("{}[SVC]{}", brand::DIM, brand::RESET),
387 _ => String::new(),
388 }
389 }
390
391 pub fn format_summary(json_result: &str) -> String {
393 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_result) {
394 let success = parsed["success"].as_bool().unwrap_or(false);
395 let total = parsed["summary"]["total_issues"].as_u64().unwrap_or(0);
396
397 if success && total == 0 {
398 format!(
399 "{}{} {} K8s manifests OK - no issues{}",
400 brand::SUCCESS,
401 icons::SUCCESS,
402 icons::KUBERNETES,
403 brand::RESET
404 )
405 } else {
406 let critical = parsed["summary"]["by_priority"]["critical"]
407 .as_u64()
408 .unwrap_or(0);
409 let high = parsed["summary"]["by_priority"]["high"]
410 .as_u64()
411 .unwrap_or(0);
412
413 if critical > 0 {
414 format!(
415 "{}{} {} {} critical, {} high priority issues{}",
416 brand::CORAL,
417 icons::CRITICAL,
418 icons::KUBERNETES,
419 critical,
420 high,
421 brand::RESET
422 )
423 } else if high > 0 {
424 format!(
425 "{}{} {} {} high priority issues{}",
426 brand::PEACH,
427 icons::HIGH,
428 icons::KUBERNETES,
429 high,
430 brand::RESET
431 )
432 } else {
433 format!(
434 "{}{} {} {} issues (medium/low){}",
435 brand::PEACH,
436 icons::MEDIUM,
437 icons::KUBERNETES,
438 total,
439 brand::RESET
440 )
441 }
442 }
443 } else {
444 format!("{} Kubelint analysis complete", icons::KUBERNETES)
445 }
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn test_format_summary_success() {
455 let json = r#"{"success": true, "summary": {"total_issues": 0, "by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}}}"#;
456 let summary = KubelintDisplay::format_summary(json);
457 assert!(summary.contains("OK"));
458 }
459
460 #[test]
461 fn test_format_summary_critical() {
462 let json = r#"{"success": false, "summary": {"total_issues": 3, "by_priority": {"critical": 1, "high": 2, "medium": 0, "low": 0}}}"#;
463 let summary = KubelintDisplay::format_summary(json);
464 assert!(summary.contains("critical"));
465 }
466
467 #[test]
468 fn test_category_badge() {
469 let badge = KubelintDisplay::get_category_badge("security");
470 assert!(badge.contains("SEC"));
471 }
472
473 #[test]
474 fn test_print_result_with_issues() {
475 let json = r#"{
477 "source": "test.yaml",
478 "success": false,
479 "decision_context": "CRITICAL security issues found.",
480 "summary": {
481 "total_issues": 2,
482 "objects_analyzed": 1,
483 "checks_run": 63,
484 "by_priority": {"critical": 1, "high": 1, "medium": 0, "low": 0}
485 },
486 "action_plan": {
487 "critical": [{
488 "check": "privileged-container",
489 "severity": "error",
490 "priority": "critical",
491 "category": "security",
492 "message": "Container running in privileged mode",
493 "line": 20,
494 "remediation": "Set privileged: false"
495 }],
496 "high": [{
497 "check": "latest-tag",
498 "severity": "warning",
499 "priority": "high",
500 "category": "best-practice",
501 "message": "Image uses :latest tag",
502 "line": 18,
503 "remediation": "Use specific tag"
504 }],
505 "medium": [],
506 "low": []
507 },
508 "quick_fixes": ["Deployment/nginx: privileged-container - Set privileged: false"]
509 }"#;
510
511 KubelintDisplay::print_result(json);
513 }
514
515 #[test]
516 fn test_print_result_success() {
517 let json = r#"{
518 "source": "secure.yaml",
519 "success": true,
520 "decision_context": "No issues found.",
521 "summary": {
522 "total_issues": 0,
523 "objects_analyzed": 3,
524 "checks_run": 63,
525 "by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}
526 },
527 "action_plan": {"critical": [], "high": [], "medium": [], "low": []}
528 }"#;
529
530 KubelintDisplay::print_result(json);
532 }
533}