1use crate::analyzer::helmlint::{HelmlintConfig, lint_chart as helmlint};
11use crate::analyzer::k8s_optimize::{
12 DataSource, K8sOptimizeConfig, LiveAnalyzer, LiveAnalyzerConfig, OutputFormat, Severity,
13 analyze, format_result,
14};
15use crate::analyzer::kubelint::{KubelintConfig, lint as kubelint};
16use crate::error::Result;
17use std::path::Path;
18
19pub struct OptimizeOptions {
21 pub cluster: Option<String>,
23 pub prometheus: Option<String>,
25 pub namespace: Option<String>,
27 pub period: String,
29 pub severity: Option<String>,
31 pub threshold: Option<u8>,
33 pub safety_margin: Option<u8>,
35 pub include_info: bool,
37 pub include_system: bool,
39 pub format: String,
41 pub output: Option<String>,
43 pub fix: bool,
45 pub full: bool,
47 pub apply: bool,
49 pub dry_run: bool,
51 pub backup_dir: Option<String>,
53 pub min_confidence: u8,
55 pub cloud_provider: Option<String>,
57 pub region: String,
59}
60
61impl Default for OptimizeOptions {
62 fn default() -> Self {
63 Self {
64 cluster: None,
65 prometheus: None,
66 namespace: None,
67 period: "7d".to_string(),
68 severity: None,
69 threshold: None,
70 safety_margin: None,
71 include_info: false,
72 include_system: false,
73 format: "table".to_string(),
74 output: None,
75 fix: false,
76 full: false,
77 apply: false,
78 dry_run: false,
79 backup_dir: None,
80 min_confidence: 70,
81 cloud_provider: None,
82 region: "us-east-1".to_string(),
83 }
84 }
85}
86
87pub async fn handle_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
89 if options.cluster.is_some() {
91 return handle_live_optimize(path, options).await;
92 }
93
94 handle_static_optimize(path, options)
96}
97
98fn handle_static_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
100 let mut config = K8sOptimizeConfig::default();
102
103 if let Some(severity_str) = &options.severity
104 && let Some(severity) = Severity::parse(severity_str)
105 {
106 config = config.with_severity(severity);
107 }
108
109 if let Some(threshold) = options.threshold {
110 config = config.with_threshold(threshold);
111 }
112
113 if let Some(margin) = options.safety_margin {
114 config = config.with_safety_margin(margin);
115 }
116
117 if options.include_info {
118 config = config.with_info();
119 }
120
121 if options.include_system {
122 config = config.with_system();
123 }
124
125 let result = analyze(path, &config);
127
128 let format = OutputFormat::parse(&options.format).unwrap_or(OutputFormat::Table);
130 let is_json = options.format == "json";
131
132 let skip_individual_output = options.full && is_json;
134
135 if !skip_individual_output {
137 if let Some(output_path) = &options.output {
138 use crate::analyzer::k8s_optimize::format_result_to_string;
140 let output = format_result_to_string(&result, format);
141 std::fs::write(output_path, output)?;
142 println!("Report written to: {}", output_path);
143 } else {
144 format_result(&result, format);
146 }
147 }
148
149 if options.full {
151 run_comprehensive_analysis(path, &result, is_json)?;
152 }
153
154 if options.fix {
156 generate_fixes(&result, path)?;
157 }
158
159 if result.summary.missing_requests > 0 || result.summary.over_provisioned > 0 {
161 }
164
165 Ok(())
166}
167
168fn run_comprehensive_analysis(
170 path: &Path,
171 resource_result: &crate::analyzer::k8s_optimize::OptimizationResult,
172 json_output: bool,
173) -> Result<()> {
174 use crate::analyzer::k8s_optimize::{
175 ChartValidation, HelmIssue, HelmValidationReport, HelmValidationSummary,
176 ResourceOptimizationReport, ResourceOptimizationSummary, SecurityFinding, SecurityReport,
177 SecuritySummary, UnifiedMetadata, UnifiedReport, UnifiedSummary,
178 };
179 use colored::Colorize;
180
181 let kubelint_config = KubelintConfig::default().with_all_builtin();
183 let kubelint_result = kubelint(path, &kubelint_config);
184
185 let helm_charts = find_helm_charts(path);
187 let helmlint_config = HelmlintConfig::default();
188 let mut chart_validations: Vec<ChartValidation> = Vec::new();
189
190 for chart_path in &helm_charts {
191 let chart_name = chart_path
192 .file_name()
193 .map(|n| n.to_string_lossy().to_string())
194 .unwrap_or_else(|| "unknown".to_string());
195
196 let helmlint_result = helmlint(chart_path, &helmlint_config);
197 chart_validations.push(ChartValidation {
198 chart_name,
199 issues: helmlint_result
200 .failures
201 .iter()
202 .map(|f| HelmIssue {
203 code: f.code.to_string(),
204 severity: format!("{:?}", f.severity).to_lowercase(),
205 message: f.message.clone(),
206 })
207 .collect(),
208 });
209 }
210
211 if json_output {
213 let critical_count = kubelint_result
214 .failures
215 .iter()
216 .filter(|f| f.severity == crate::analyzer::kubelint::Severity::Error)
217 .count();
218 let warning_count = kubelint_result.failures.len() - critical_count;
219 let helm_issues: usize = chart_validations.iter().map(|c| c.issues.len()).sum();
220
221 let report = UnifiedReport {
222 summary: UnifiedSummary {
223 total_resources: resource_result.summary.resources_analyzed as usize
224 + kubelint_result.summary.objects_analyzed,
225 total_issues: resource_result.recommendations.len()
226 + kubelint_result.failures.len()
227 + helm_issues,
228 critical_issues: resource_result
229 .recommendations
230 .iter()
231 .filter(|r| r.severity == crate::analyzer::k8s_optimize::Severity::Critical)
232 .count()
233 + critical_count,
234 high_issues: resource_result
235 .recommendations
236 .iter()
237 .filter(|r| r.severity == crate::analyzer::k8s_optimize::Severity::High)
238 .count(),
239 medium_issues: resource_result
240 .recommendations
241 .iter()
242 .filter(|r| r.severity == crate::analyzer::k8s_optimize::Severity::Medium)
243 .count()
244 + warning_count,
245 confidence: 60, health_score: calculate_health_score(
247 resource_result,
248 &kubelint_result,
249 &chart_validations,
250 ),
251 },
252 live_analysis: None,
253 resource_optimization: ResourceOptimizationReport {
254 summary: ResourceOptimizationSummary {
255 resources: resource_result.summary.resources_analyzed as usize,
256 containers: resource_result.summary.containers_analyzed as usize,
257 over_provisioned: resource_result.summary.over_provisioned as usize,
258 missing_requests: resource_result.summary.missing_requests as usize,
259 optimal: resource_result.summary.optimal as usize,
260 estimated_waste_percent: resource_result.summary.total_waste_percentage,
261 },
262 recommendations: resource_result.recommendations.clone(),
263 },
264 security: SecurityReport {
265 summary: SecuritySummary {
266 objects_analyzed: kubelint_result.summary.objects_analyzed,
267 checks_run: kubelint_result.summary.checks_run,
268 critical: critical_count,
269 warnings: warning_count,
270 },
271 findings: kubelint_result
272 .failures
273 .iter()
274 .map(|f| SecurityFinding {
275 code: f.code.to_string(),
276 severity: format!("{:?}", f.severity).to_lowercase(),
277 object_kind: f.object_kind.clone(),
278 object_name: f.object_name.clone(),
279 message: f.message.clone(),
280 remediation: f.remediation.clone(),
281 })
282 .collect(),
283 },
284 helm_validation: HelmValidationReport {
285 summary: HelmValidationSummary {
286 charts_analyzed: chart_validations.len(),
287 charts_with_issues: chart_validations
288 .iter()
289 .filter(|c| !c.issues.is_empty())
290 .count(),
291 total_issues: helm_issues,
292 },
293 charts: chart_validations,
294 },
295 live_fixes: None, trend_analysis: None,
297 cost_estimation: None,
298 precise_fixes: None,
299 metadata: UnifiedMetadata {
300 path: path.display().to_string(),
301 analysis_time_ms: resource_result.metadata.duration_ms,
302 timestamp: chrono::Utc::now().to_rfc3339(),
303 version: env!("CARGO_PKG_VERSION").to_string(),
304 },
305 };
306
307 println!(
308 "{}",
309 serde_json::to_string_pretty(&report).unwrap_or_default()
310 );
311 return Ok(());
312 }
313
314 println!("\n{}", "═".repeat(91).bright_blue());
316 println!(
317 "{}",
318 "🔒 SECURITY & BEST PRACTICES ANALYSIS (kubelint)"
319 .bright_blue()
320 .bold()
321 );
322 println!("{}\n", "═".repeat(91).bright_blue());
323
324 if kubelint_result.failures.is_empty() {
325 println!(
326 "{} No security or best practice issues found!\n",
327 "✅".green()
328 );
329 } else {
330 let critical: Vec<_> = kubelint_result
332 .failures
333 .iter()
334 .filter(|f| f.severity == crate::analyzer::kubelint::Severity::Error)
335 .collect();
336 let warnings: Vec<_> = kubelint_result
337 .failures
338 .iter()
339 .filter(|f| f.severity == crate::analyzer::kubelint::Severity::Warning)
340 .collect();
341
342 println!(
343 "┌─ Summary ─────────────────────────────────────────────────────────────────────────────────┐"
344 );
345 println!(
346 "│ Objects analyzed: {:>3} Checks run: {:>3} Issues: {:>3}",
347 kubelint_result.summary.objects_analyzed,
348 kubelint_result.summary.checks_run,
349 kubelint_result.failures.len()
350 );
351 println!(
352 "│ Critical: {:>3} Warnings: {:>3}",
353 critical.len(),
354 warnings.len()
355 );
356 println!(
357 "└───────────────────────────────────────────────────────────────────────────────────────────┘\n"
358 );
359
360 for failure in critical.iter().take(10) {
362 println!(
363 "🔴 {} {}/{}",
364 format!("[{}]", failure.code).red().bold(),
365 failure.object_kind,
366 failure.object_name
367 );
368 println!(" {}", failure.message);
369 if let Some(remediation) = &failure.remediation {
370 println!(" {} {}", "Fix:".yellow(), remediation);
371 }
372 println!();
373 }
374
375 for failure in warnings.iter().take(5) {
377 println!(
378 "🟡 {} {}/{}",
379 format!("[{}]", failure.code).yellow(),
380 failure.object_kind,
381 failure.object_name
382 );
383 println!(" {}", failure.message);
384 println!();
385 }
386
387 if warnings.len() > 5 {
388 println!(" ... and {} more warnings\n", warnings.len() - 5);
389 }
390 }
391
392 if !helm_charts.is_empty() {
394 println!("\n{}", "═".repeat(91).bright_cyan());
395 println!(
396 "{}",
397 "📦 HELM CHART VALIDATION (helmlint)".bright_cyan().bold()
398 );
399 println!("{}\n", "═".repeat(91).bright_cyan());
400
401 for chart in &chart_validations {
402 if chart.issues.is_empty() {
403 println!("{} {} - No issues found", "✅".green(), chart.chart_name);
404 } else {
405 println!(
406 "{} {} - {} issues found",
407 "⚠️".yellow(),
408 chart.chart_name,
409 chart.issues.len()
410 );
411
412 for issue in chart.issues.iter().take(3) {
413 println!(
414 " {} {}",
415 format!("[{}]", issue.code).yellow(),
416 issue.message
417 );
418 }
419 if chart.issues.len() > 3 {
420 println!(" ... and {} more\n", chart.issues.len() - 3);
421 }
422 }
423 }
424 println!();
425 }
426
427 Ok(())
428}
429
430fn calculate_health_score(
432 resource_result: &crate::analyzer::k8s_optimize::OptimizationResult,
433 kubelint_result: &crate::analyzer::kubelint::LintResult,
434 helm_validations: &[crate::analyzer::k8s_optimize::ChartValidation],
435) -> u8 {
436 let total_resources = resource_result.summary.resources_analyzed.max(1) as f32;
437 let optimal_resources = resource_result.summary.optimal as f32;
438
439 let resource_score = (optimal_resources / total_resources) * 40.0;
441
442 let security_objects = kubelint_result.summary.objects_analyzed.max(1) as f32;
444 let security_issues = kubelint_result.failures.len() as f32;
445 let security_score =
446 ((security_objects - security_issues.min(security_objects)) / security_objects) * 40.0;
447
448 let total_charts = helm_validations.len().max(1) as f32;
450 let charts_with_issues = helm_validations
451 .iter()
452 .filter(|c| !c.issues.is_empty())
453 .count() as f32;
454 let helm_score = ((total_charts - charts_with_issues) / total_charts) * 20.0;
455
456 (resource_score + security_score + helm_score).round() as u8
457}
458
459fn find_helm_charts(path: &Path) -> Vec<std::path::PathBuf> {
461 let mut charts = Vec::new();
462
463 if path.join("Chart.yaml").exists() {
464 charts.push(path.to_path_buf());
465 return charts;
466 }
467
468 if let Ok(entries) = std::fs::read_dir(path) {
469 for entry in entries.flatten() {
470 let entry_path = entry.path();
471 if entry_path.is_dir() {
472 if entry_path.join("Chart.yaml").exists() {
473 charts.push(entry_path);
474 } else {
475 if let Ok(sub_entries) = std::fs::read_dir(&entry_path) {
477 for sub_entry in sub_entries.flatten() {
478 let sub_path = sub_entry.path();
479 if sub_path.is_dir() && sub_path.join("Chart.yaml").exists() {
480 charts.push(sub_path);
481 }
482 }
483 }
484 }
485 }
486 }
487 }
488
489 charts
490}
491
492fn generate_fixes(
494 result: &crate::analyzer::k8s_optimize::OptimizationResult,
495 _base_path: &Path,
496) -> Result<()> {
497 if result.recommendations.is_empty() {
498 println!("No fixes to generate - all resources are well-configured!");
499 return Ok(());
500 }
501
502 println!("\n\u{1F4DD} Suggested fixes:\n");
503
504 for rec in &result.recommendations {
505 println!(
506 "# {} ({}/{})",
507 rec.resource_identifier(),
508 rec.resource_kind,
509 rec.container
510 );
511 println!("{}", rec.fix_yaml);
512 println!();
513 }
514
515 println!("Apply these changes to your manifest files to optimize resource allocation.");
516
517 Ok(())
518}
519
520async fn handle_live_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
522 use colored::Colorize;
523
524 let _ = rustls::crypto::ring::default_provider().install_default();
526
527 let cluster_context = options
528 .cluster
529 .clone()
530 .unwrap_or_else(|| "current".to_string());
531 let is_json = options.format.to_lowercase() == "json";
532
533 if !is_json {
534 println!("\n\u{2601}\u{FE0F} Connecting to Kubernetes cluster...\n");
535 }
536
537 let live_config = LiveAnalyzerConfig {
539 prometheus_url: options.prometheus.clone(),
540 history_period: options.period.clone(),
541 safety_margin_pct: options.safety_margin.unwrap_or(20),
542 min_samples: 100,
543 waste_threshold_pct: options.threshold.map(|t| t as f32).unwrap_or(10.0),
544 namespace: options.namespace.clone(),
545 include_system: options.include_system,
546 };
547
548 let analyzer = if cluster_context == "current" || cluster_context.is_empty() {
550 LiveAnalyzer::new(live_config).await
551 } else {
552 LiveAnalyzer::with_context(&cluster_context, live_config).await
553 }
554 .map_err(|e| {
555 crate::error::IaCGeneratorError::Io(std::io::Error::other(format!(
556 "Failed to connect to cluster: {}",
557 e
558 )))
559 })?;
560
561 let sources = analyzer.available_sources().await;
563
564 if !is_json {
565 println!("\u{1F4CA} Available data sources:");
566 for source in &sources {
567 let (icon, name) = match source {
568 DataSource::MetricsServer => ("\u{1F4C8}", "metrics-server (real-time)"),
569 DataSource::Prometheus => ("\u{1F4CA}", "Prometheus (historical)"),
570 DataSource::Combined => ("\u{2728}", "Combined (highest accuracy)"),
571 DataSource::Static => ("\u{1F4C4}", "Static (heuristics only)"),
572 };
573 println!(" {} {}", icon, name);
574 }
575 println!();
576 }
577
578 let result = analyzer.analyze().await.map_err(|e| {
580 crate::error::IaCGeneratorError::Io(std::io::Error::other(format!(
581 "Analysis failed: {}",
582 e
583 )))
584 })?;
585
586 if !is_json {
588 let source_name = match result.source {
589 DataSource::Combined => "Combined (Prometheus + metrics-server)"
590 .bright_green()
591 .to_string(),
592 DataSource::Prometheus => "Prometheus (historical data)".green().to_string(),
593 DataSource::MetricsServer => "metrics-server (real-time snapshot)".yellow().to_string(),
594 DataSource::Static => "Static heuristics (no cluster data)".red().to_string(),
595 };
596
597 println!("\n\u{1F50E} Analysis Results (Source: {})\n", source_name);
598 println!("{}\n", "=".repeat(70).bright_blue());
599
600 println!("\u{1F4CA} Summary:");
602 println!(
603 " Resources analyzed: {}",
604 result.summary.resources_analyzed
605 );
606 println!(
607 " Over-provisioned: {} {}",
608 result.summary.over_provisioned,
609 if result.summary.over_provisioned > 0 {
610 "\u{26A0}\u{FE0F}"
611 } else {
612 "\u{2705}"
613 }
614 );
615 println!(
616 " Under-provisioned: {} {}",
617 result.summary.under_provisioned,
618 if result.summary.under_provisioned > 0 {
619 "\u{1F6A8}"
620 } else {
621 "\u{2705}"
622 }
623 );
624 println!(" Optimal: {}", result.summary.optimal);
625 println!(" Confidence: {}%", result.summary.confidence);
626
627 if result.summary.total_cpu_waste_millicores > 0
629 || result.summary.total_memory_waste_bytes > 0
630 {
631 println!("\n\u{1F4B8} Waste Summary:");
632 if result.summary.total_cpu_waste_millicores > 0 {
633 let cores = result.summary.total_cpu_waste_millicores as f64 / 1000.0;
634 println!(" CPU wasted: {:.2} cores", cores);
635 }
636 if result.summary.total_memory_waste_bytes > 0 {
637 let gb =
638 result.summary.total_memory_waste_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
639 println!(" Memory wasted: {:.2} GB", gb);
640 }
641 }
642
643 if !result.recommendations.is_empty() {
645 println!("\n\u{1F4DD} Recommendations:\n");
646 println!(
647 "{:<40} {:>10} {:>10} {:>8} {:>8}",
648 "Workload", "CPU Waste", "Mem Waste", "Conf", "Severity"
649 );
650 println!("{}", "-".repeat(80));
651
652 for rec in &result.recommendations {
653 let severity_str = match rec.severity {
654 Severity::Critical => "CRIT".red().bold().to_string(),
655 Severity::High => "HIGH".red().to_string(),
656 Severity::Medium => "MED".yellow().to_string(),
657 Severity::Low => "LOW".blue().to_string(),
658 Severity::Info => "INFO".dimmed().to_string(),
659 };
660
661 let workload = format!("{}/{}", rec.namespace, rec.workload_name);
662 let workload_display = if workload.len() > 38 {
663 format!("...{}", &workload[workload.len() - 35..])
664 } else {
665 workload
666 };
667
668 println!(
669 "{:<40} {:>9.0}% {:>9.0}% {:>7}% {:>8}",
670 workload_display,
671 rec.cpu_waste_pct,
672 rec.memory_waste_pct,
673 rec.confidence,
674 severity_str
675 );
676
677 let cpu_rec = format_millicores(rec.recommended_cpu_millicores);
679 let mem_rec = format_bytes(rec.recommended_memory_bytes);
680 println!(
681 " {} CPU: {} -> {} | Memory: {} -> {}",
682 "\u{27A1}\u{FE0F}".dimmed(),
683 rec.current_cpu_millicores
684 .map(format_millicores)
685 .unwrap_or_else(|| "none".to_string())
686 .red(),
687 cpu_rec.green(),
688 rec.current_memory_bytes
689 .map(format_bytes)
690 .unwrap_or_else(|| "none".to_string())
691 .red(),
692 mem_rec.green()
693 );
694 }
695 }
696
697 for warning in &result.warnings {
699 println!("\n\u{26A0}\u{FE0F} {}", warning.yellow());
700 }
701 }
702
703 if path.exists() && path.is_dir() {
705 if options.full && is_json {
706 run_comprehensive_analysis_with_live(path, &result, &options)?;
708 } else {
709 if !is_json {
710 println!(
711 "\n\u{1F4C1} Also checking local manifests in: {}\n",
712 path.display()
713 );
714 }
715 let _ = handle_static_optimize(
716 path,
717 OptimizeOptions {
718 cluster: None,
719 prometheus: None,
720 namespace: None,
721 period: "7d".to_string(),
722 severity: options.severity.clone(),
723 threshold: options.threshold,
724 safety_margin: options.safety_margin,
725 include_info: options.include_info,
726 include_system: options.include_system,
727 format: options.format.clone(),
728 output: None,
729 fix: false,
730 full: options.full,
731 apply: false,
732 dry_run: options.dry_run,
733 backup_dir: None,
734 min_confidence: options.min_confidence,
735 cloud_provider: options.cloud_provider.clone(),
736 region: options.region.clone(),
737 },
738 );
739 }
740 } else if options.full && is_json {
741 run_live_only_unified_report(&result)?;
743 }
744
745 if let Some(output_path) = &options.output {
747 let json = serde_json::to_string_pretty(&result).map_err(|e| {
748 crate::error::IaCGeneratorError::Io(std::io::Error::other(format!(
749 "Failed to serialize result: {}",
750 e
751 )))
752 })?;
753 std::fs::write(output_path, json)?;
754 if !is_json {
755 println!("\n\u{1F4BE} Report saved to: {}", output_path);
756 }
757 }
758
759 Ok(())
760}
761
762fn run_comprehensive_analysis_with_live(
764 path: &Path,
765 live_result: &crate::analyzer::k8s_optimize::LiveAnalysisResult,
766 options: &OptimizeOptions,
767) -> Result<()> {
768 use crate::analyzer::k8s_optimize::{
769 ChartValidation, CloudProvider, HelmIssue, HelmValidationReport, HelmValidationSummary,
770 LiveClusterSummary, ResourceOptimizationReport, ResourceOptimizationSummary,
771 SecurityFinding, SecurityReport, SecuritySummary, UnifiedMetadata, UnifiedReport,
772 UnifiedSummary, analyze_trends_from_live, calculate_from_live,
773 locate_resources_from_static,
774 };
775
776 let static_config = K8sOptimizeConfig::default();
778 let resource_result = analyze(path, &static_config);
779
780 let kubelint_config = KubelintConfig::default().with_all_builtin();
782 let kubelint_result = kubelint(path, &kubelint_config);
783
784 let helm_charts = find_helm_charts(path);
786 let helmlint_config = HelmlintConfig::default();
787 let mut chart_validations: Vec<ChartValidation> = Vec::new();
788
789 for chart_path in &helm_charts {
790 let chart_name = chart_path
791 .file_name()
792 .map(|n| n.to_string_lossy().to_string())
793 .unwrap_or_else(|| "unknown".to_string());
794
795 let helmlint_result = helmlint(chart_path, &helmlint_config);
796 chart_validations.push(ChartValidation {
797 chart_name,
798 issues: helmlint_result
799 .failures
800 .iter()
801 .map(|f| HelmIssue {
802 code: f.code.to_string(),
803 severity: format!("{:?}", f.severity).to_lowercase(),
804 message: f.message.clone(),
805 })
806 .collect(),
807 });
808 }
809
810 let uses_prometheus = matches!(
812 live_result.source,
813 DataSource::Prometheus | DataSource::Combined
814 );
815 let live_summary = LiveClusterSummary {
816 source: format!("{:?}", live_result.source),
817 resources_analyzed: live_result.summary.resources_analyzed,
818 over_provisioned: live_result.summary.over_provisioned,
819 under_provisioned: live_result.summary.under_provisioned,
820 optimal: live_result.summary.optimal,
821 confidence: live_result.summary.confidence,
822 uses_p95: if uses_prometheus { Some(true) } else { None },
823 history_period: if uses_prometheus {
824 Some(options.period.clone())
825 } else {
826 None
827 },
828 };
829
830 let (deduplicated_recs, dedup_stats) = deduplicate_recommendations(
833 &live_result.recommendations,
834 &resource_result.recommendations,
835 );
836
837 let live_analyzed = live_result.summary.resources_analyzed;
839 let static_analyzed = resource_result.summary.resources_analyzed as usize;
840 let total_resources = std::cmp::max(live_analyzed, static_analyzed);
841
842 let resource_issues = deduplicated_recs.len();
844 let security_issues = kubelint_result.failures.len();
845 let helm_issues: usize = chart_validations.iter().map(|h| h.issues.len()).sum();
846 let total_issues = resource_issues + security_issues + helm_issues;
847
848 if dedup_stats.duplicates_removed > 0 {
850 eprintln!(
851 "📊 Deduplication: {} duplicates removed, {} corroborated findings",
852 dedup_stats.duplicates_removed, dedup_stats.corroborated
853 );
854 }
855
856 let mut critical = 0usize;
858 let mut high = 0usize;
859 let mut medium = 0usize;
860
861 for rec in &live_result.recommendations {
863 match rec.severity {
864 crate::analyzer::k8s_optimize::Severity::Critical => critical += 1,
865 crate::analyzer::k8s_optimize::Severity::High => high += 1,
866 crate::analyzer::k8s_optimize::Severity::Medium => medium += 1,
867 _ => {}
868 }
869 }
870
871 for rec in &resource_result.recommendations {
873 match rec.severity {
874 crate::analyzer::k8s_optimize::Severity::Critical => critical += 1,
875 crate::analyzer::k8s_optimize::Severity::High => high += 1,
876 crate::analyzer::k8s_optimize::Severity::Medium => medium += 1,
877 _ => {}
878 }
879 }
880
881 for f in &kubelint_result.failures {
883 if f.severity == crate::analyzer::kubelint::Severity::Error {
884 critical += 1;
885 } else if f.severity == crate::analyzer::kubelint::Severity::Warning {
886 medium += 1;
887 }
888 }
889
890 let confidence = if live_result.summary.confidence > 0 {
892 live_result.summary.confidence
893 } else {
894 calculate_health_score(&resource_result, &kubelint_result, &chart_validations)
895 };
896
897 let health_score =
898 calculate_health_score(&resource_result, &kubelint_result, &chart_validations);
899
900 let report = UnifiedReport {
902 summary: UnifiedSummary {
903 total_resources,
904 total_issues,
905 critical_issues: critical,
906 high_issues: high,
907 medium_issues: medium,
908 confidence,
909 health_score,
910 },
911 live_analysis: Some(live_summary),
912 resource_optimization: ResourceOptimizationReport {
913 summary: ResourceOptimizationSummary {
914 resources: resource_result.summary.resources_analyzed as usize,
915 containers: resource_result.summary.containers_analyzed as usize,
916 over_provisioned: resource_result.summary.over_provisioned as usize,
917 missing_requests: resource_result.summary.missing_requests as usize,
918 optimal: resource_result.summary.optimal as usize,
919 estimated_waste_percent: resource_result.summary.total_waste_percentage,
920 },
921 recommendations: resource_result.recommendations.clone(),
922 },
923 security: SecurityReport {
924 summary: SecuritySummary {
925 objects_analyzed: kubelint_result.summary.objects_analyzed,
926 checks_run: kubelint_result.summary.checks_run,
927 critical: kubelint_result
928 .failures
929 .iter()
930 .filter(|f| f.severity == crate::analyzer::kubelint::Severity::Error)
931 .count(),
932 warnings: kubelint_result.failures.len(),
933 },
934 findings: kubelint_result
935 .failures
936 .iter()
937 .map(|f| SecurityFinding {
938 code: f.code.to_string(),
939 severity: format!("{:?}", f.severity).to_lowercase(),
940 object_kind: f.object_kind.clone(),
941 object_name: f.object_name.clone(),
942 message: f.message.clone(),
943 remediation: f.remediation.clone(),
944 })
945 .collect(),
946 },
947 helm_validation: HelmValidationReport {
948 summary: HelmValidationSummary {
949 charts_analyzed: chart_validations.len(),
950 charts_with_issues: chart_validations
951 .iter()
952 .filter(|c| !c.issues.is_empty())
953 .count(),
954 total_issues: helm_issues,
955 },
956 charts: chart_validations,
957 },
958 live_fixes: if live_result.recommendations.is_empty() {
959 None
960 } else {
961 Some(
962 live_result
963 .recommendations
964 .iter()
965 .map(|rec| crate::analyzer::k8s_optimize::LiveFix {
966 namespace: rec.namespace.clone(),
967 workload_name: rec.workload_name.clone(),
968 container_name: rec.container_name.clone(),
969 confidence: rec.confidence,
970 source: format!("{:?}", rec.data_source),
971 fix_yaml: rec.generate_fix_yaml(),
972 })
973 .collect(),
974 )
975 },
976 trend_analysis: Some(analyze_trends_from_live(&live_result.recommendations)),
977 cost_estimation: {
978 let provider = match options.cloud_provider.as_deref() {
980 Some("aws") => CloudProvider::Aws,
981 Some("gcp") => CloudProvider::Gcp,
982 Some("azure") => CloudProvider::Azure,
983 Some("onprem") => CloudProvider::OnPrem,
984 _ => CloudProvider::Unknown,
985 };
986 Some(calculate_from_live(
987 &live_result.recommendations,
988 provider,
989 &options.region,
990 ))
991 },
992 precise_fixes: {
993 let fixes = locate_resources_from_static(&resource_result.recommendations);
994 if fixes.is_empty() { None } else { Some(fixes) }
995 },
996 metadata: UnifiedMetadata {
997 path: path.display().to_string(),
998 analysis_time_ms: resource_result.metadata.duration_ms,
999 timestamp: chrono::Utc::now().to_rfc3339(),
1000 version: env!("CARGO_PKG_VERSION").to_string(),
1001 },
1002 };
1003
1004 println!(
1006 "{}",
1007 serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
1008 );
1009
1010 Ok(())
1011}
1012
1013fn run_live_only_unified_report(
1015 live_result: &crate::analyzer::k8s_optimize::LiveAnalysisResult,
1016) -> Result<()> {
1017 use crate::analyzer::k8s_optimize::{
1018 HelmValidationReport, HelmValidationSummary, LiveClusterSummary,
1019 ResourceOptimizationReport, ResourceOptimizationSummary, SecurityReport, SecuritySummary,
1020 UnifiedMetadata, UnifiedReport, UnifiedSummary, analyze_trends_from_live,
1021 };
1022
1023 let uses_prometheus = matches!(
1024 live_result.source,
1025 crate::analyzer::k8s_optimize::DataSource::Prometheus
1026 | crate::analyzer::k8s_optimize::DataSource::Combined
1027 );
1028 let live_summary = LiveClusterSummary {
1029 source: format!("{:?}", live_result.source),
1030 resources_analyzed: live_result.summary.resources_analyzed,
1031 over_provisioned: live_result.summary.over_provisioned,
1032 under_provisioned: live_result.summary.under_provisioned,
1033 optimal: live_result.summary.optimal,
1034 confidence: live_result.summary.confidence,
1035 uses_p95: if uses_prometheus { Some(true) } else { None },
1036 history_period: None, };
1038
1039 let mut critical = 0;
1041 let mut high = 0;
1042 let mut medium = 0;
1043 for rec in &live_result.recommendations {
1044 match rec.severity {
1045 crate::analyzer::k8s_optimize::Severity::Critical => critical += 1,
1046 crate::analyzer::k8s_optimize::Severity::High => high += 1,
1047 crate::analyzer::k8s_optimize::Severity::Medium => medium += 1,
1048 _ => {}
1049 }
1050 }
1051
1052 let report = UnifiedReport {
1053 summary: UnifiedSummary {
1054 total_resources: live_result.summary.resources_analyzed,
1055 total_issues: live_result.recommendations.len(),
1056 critical_issues: critical,
1057 high_issues: high,
1058 medium_issues: medium,
1059 confidence: live_result.summary.confidence,
1060 health_score: if live_result.recommendations.is_empty() {
1061 100
1062 } else {
1063 (100 - std::cmp::min(critical * 15 + high * 10 + medium * 3, 100)) as u8
1064 },
1065 },
1066 live_analysis: Some(live_summary),
1067 resource_optimization: ResourceOptimizationReport {
1068 summary: ResourceOptimizationSummary {
1069 resources: live_result.summary.resources_analyzed,
1070 containers: live_result.recommendations.len(),
1071 over_provisioned: live_result.summary.over_provisioned,
1072 missing_requests: 0,
1073 optimal: live_result.summary.optimal,
1074 estimated_waste_percent: 0.0,
1075 },
1076 recommendations: vec![],
1077 },
1078 security: SecurityReport {
1079 summary: SecuritySummary {
1080 objects_analyzed: 0,
1081 checks_run: 0,
1082 critical: 0,
1083 warnings: 0,
1084 },
1085 findings: vec![],
1086 },
1087 helm_validation: HelmValidationReport {
1088 summary: HelmValidationSummary {
1089 charts_analyzed: 0,
1090 charts_with_issues: 0,
1091 total_issues: 0,
1092 },
1093 charts: vec![],
1094 },
1095 live_fixes: if live_result.recommendations.is_empty() {
1096 None
1097 } else {
1098 Some(
1099 live_result
1100 .recommendations
1101 .iter()
1102 .map(|rec| crate::analyzer::k8s_optimize::LiveFix {
1103 namespace: rec.namespace.clone(),
1104 workload_name: rec.workload_name.clone(),
1105 container_name: rec.container_name.clone(),
1106 confidence: rec.confidence,
1107 source: format!("{:?}", rec.data_source),
1108 fix_yaml: rec.generate_fix_yaml(),
1109 })
1110 .collect(),
1111 )
1112 },
1113 trend_analysis: Some(analyze_trends_from_live(&live_result.recommendations)),
1114 cost_estimation: None, precise_fixes: None, metadata: UnifiedMetadata {
1117 path: "cluster-only".to_string(),
1118 analysis_time_ms: 0,
1119 timestamp: chrono::Utc::now().to_rfc3339(),
1120 version: env!("CARGO_PKG_VERSION").to_string(),
1121 },
1122 };
1123
1124 println!(
1125 "{}",
1126 serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
1127 );
1128
1129 Ok(())
1130}
1131
1132struct DeduplicationStats {
1134 duplicates_removed: usize,
1135 corroborated: usize,
1136}
1137
1138#[derive(Debug, Clone)]
1140#[allow(dead_code)] struct MergedRecommendation {
1142 namespace: String,
1143 workload_name: String,
1144 container_name: String,
1145 severity: crate::analyzer::k8s_optimize::Severity,
1146 confidence: u8,
1148 source: RecommendationSource,
1150 cpu_waste_pct: f32,
1152 memory_waste_pct: f32,
1154}
1155
1156#[derive(Debug, Clone, PartialEq)]
1157enum RecommendationSource {
1158 LiveOnly,
1159 StaticOnly,
1160 Corroborated,
1161}
1162
1163fn deduplicate_recommendations(
1166 live_recs: &[crate::analyzer::k8s_optimize::LiveRecommendation],
1167 static_recs: &[crate::analyzer::k8s_optimize::ResourceRecommendation],
1168) -> (Vec<MergedRecommendation>, DeduplicationStats) {
1169 use std::collections::HashMap;
1170
1171 let mut merged: HashMap<(String, String, String), MergedRecommendation> = HashMap::new();
1172 let mut stats = DeduplicationStats {
1173 duplicates_removed: 0,
1174 corroborated: 0,
1175 };
1176
1177 for rec in live_recs {
1179 let key = (
1180 rec.namespace.clone(),
1181 rec.workload_name.clone(),
1182 rec.container_name.clone(),
1183 );
1184 merged.insert(
1185 key,
1186 MergedRecommendation {
1187 namespace: rec.namespace.clone(),
1188 workload_name: rec.workload_name.clone(),
1189 container_name: rec.container_name.clone(),
1190 severity: rec.severity,
1191 confidence: rec.confidence,
1192 source: RecommendationSource::LiveOnly,
1193 cpu_waste_pct: rec.cpu_waste_pct,
1194 memory_waste_pct: rec.memory_waste_pct,
1195 },
1196 );
1197 }
1198
1199 for rec in static_recs {
1201 let ns = rec
1202 .namespace
1203 .clone()
1204 .unwrap_or_else(|| "default".to_string());
1205 let key = (ns.clone(), rec.resource_name.clone(), rec.container.clone());
1206
1207 if let Some(existing) = merged.get_mut(&key) {
1208 existing.confidence = std::cmp::min(existing.confidence + 10, 100);
1211 existing.source = RecommendationSource::Corroborated;
1212 stats.duplicates_removed += 1;
1213 stats.corroborated += 1;
1214 } else {
1215 merged.insert(
1217 key,
1218 MergedRecommendation {
1219 namespace: ns,
1220 workload_name: rec.resource_name.clone(),
1221 container_name: rec.container.clone(),
1222 severity: rec.severity,
1223 confidence: 50, source: RecommendationSource::StaticOnly,
1225 cpu_waste_pct: 0.0, memory_waste_pct: 0.0,
1227 },
1228 );
1229 }
1230 }
1231
1232 (merged.into_values().collect(), stats)
1233}
1234
1235fn format_millicores(millicores: u64) -> String {
1237 if millicores >= 1000 {
1238 format!("{:.1}", millicores as f64 / 1000.0)
1239 } else {
1240 format!("{}m", millicores)
1241 }
1242}
1243
1244fn format_bytes(bytes: u64) -> String {
1246 const GI: u64 = 1024 * 1024 * 1024;
1247 const MI: u64 = 1024 * 1024;
1248
1249 if bytes >= GI {
1250 format!("{:.1}Gi", bytes as f64 / GI as f64)
1251 } else {
1252 format!("{}Mi", bytes / MI)
1253 }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258 use super::*;
1259 use std::path::PathBuf;
1260
1261 #[tokio::test]
1262 async fn test_handle_optimize_nonexistent_path() {
1263 let result = handle_optimize(
1264 &PathBuf::from("/nonexistent/path"),
1265 OptimizeOptions::default(),
1266 )
1267 .await;
1268 assert!(result.is_ok());
1270 }
1271}