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