syncable_cli/handlers/
optimize.rs

1//! Handler for the `optimize` command.
2//!
3//! Analyzes Kubernetes manifests for resource optimization opportunities.
4//! Supports both static analysis (Phase 1) and live cluster analysis (Phase 2).
5//!
6//! With `--full` flag, also runs:
7//! - kubelint: Security and best practice checks
8//! - helmlint: Helm chart structure validation
9
10use 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
19/// Configuration for the optimize command
20pub struct OptimizeOptions {
21    /// Connect to a live cluster (context name or empty for current)
22    pub cluster: Option<String>,
23    /// Prometheus URL for historical metrics
24    pub prometheus: Option<String>,
25    /// Target namespace
26    pub namespace: Option<String>,
27    /// Analysis period for historical data
28    pub period: String,
29    /// Minimum severity to report
30    pub severity: Option<String>,
31    /// Minimum waste percentage to report
32    pub threshold: Option<u8>,
33    /// Safety margin percentage
34    pub safety_margin: Option<u8>,
35    /// Include info-level suggestions
36    pub include_info: bool,
37    /// Include system namespaces
38    pub include_system: bool,
39    /// Output format
40    pub format: String,
41    /// Output file
42    pub output: Option<String>,
43    /// Generate fixes
44    pub fix: bool,
45    /// Run comprehensive analysis (kubelint + helmlint + optimize)
46    pub full: bool,
47    /// Apply fixes to manifest files
48    pub apply: bool,
49    /// Dry-run mode (preview without applying)
50    pub dry_run: bool,
51    /// Backup directory for original files
52    pub backup_dir: Option<String>,
53    /// Minimum confidence threshold for auto-apply
54    pub min_confidence: u8,
55    /// Cloud provider for cost estimation
56    pub cloud_provider: Option<String>,
57    /// Region for cloud pricing
58    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
87/// Handle the `optimize` command.
88pub async fn handle_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
89    // Check if we should use live cluster analysis
90    if options.cluster.is_some() {
91        return handle_live_optimize(path, options).await;
92    }
93
94    // Static analysis mode (Phase 1)
95    handle_static_optimize(path, options)
96}
97
98/// Handle static analysis (Phase 1) - analyzes manifests without cluster connection.
99fn handle_static_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
100    // Build config
101    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    // Run resource optimization analysis
126    let result = analyze(path, &config);
127
128    // Determine output format
129    let format = OutputFormat::parse(&options.format).unwrap_or(OutputFormat::Table);
130    let is_json = options.format == "json";
131
132    // If using --full with JSON, skip individual output and only show unified report
133    let skip_individual_output = options.full && is_json;
134
135    // Output resource optimization result (unless skipping for unified JSON)
136    if !skip_individual_output {
137        if let Some(output_path) = &options.output {
138            // Write to file
139            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            // Print to stdout
145            format_result(&result, format);
146        }
147    }
148
149    // Run comprehensive analysis if --full flag is set
150    if options.full {
151        run_comprehensive_analysis(path, &result, is_json)?;
152    }
153
154    // Generate fixes if requested
155    if options.fix {
156        generate_fixes(&result, path)?;
157    }
158
159    // Exit with non-zero if critical issues found
160    if result.summary.missing_requests > 0 || result.summary.over_provisioned > 0 {
161        // We could exit with error here for CI/CD
162        // std::process::exit(1);
163    }
164
165    Ok(())
166}
167
168/// Run comprehensive analysis with kubelint and helmlint.
169fn 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    // Run kubelint
182    let kubelint_config = KubelintConfig::default().with_all_builtin();
183    let kubelint_result = kubelint(path, &kubelint_config);
184
185    // Run helmlint on all charts
186    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, build unified report and print
212    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, // Static analysis confidence
246                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, // No live data in static-only analysis
296            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    // Table output (existing code)
315    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        // Group by priority
331        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        // Show critical issues
361        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        // Show warnings (limited)
376        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    // Helm chart validation output
393    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
430/// Calculate an overall health score based on all findings.
431fn 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    // Start with resource optimization score (40% weight)
440    let resource_score = (optimal_resources / total_resources) * 40.0;
441
442    // Security score (40% weight)
443    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    // Helm validation score (20% weight)
449    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
459/// Find Helm charts in a directory.
460fn 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                    // Check one level deeper
476                    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
492/// Generate optimized manifest files.
493fn 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
520/// Handle live cluster analysis (Phase 2) - connects to cluster for real metrics.
521async fn handle_live_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
522    use colored::Colorize;
523
524    // Install rustls crypto provider (required for TLS connections to K8s API)
525    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    // Build live analyzer config
538    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    // Create analyzer (with context or default)
549    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    // Check available data sources
562    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    // Run analysis
579    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    // Display results (only in non-JSON mode)
587    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        // Summary
601        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        // Waste summary
628        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        // Recommendations
644        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                // Show recommended values
678                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        // Warnings
698        for warning in &result.warnings {
699            println!("\n\u{26A0}\u{FE0F}  {}", warning.yellow());
700        }
701    }
702
703    // Also run static analysis on manifests if path provided
704    if path.exists() && path.is_dir() {
705        if options.full && is_json {
706            // Run comprehensive analysis and output unified JSON with live data
707            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        // Output live-only unified report
742        run_live_only_unified_report(&result)?;
743    }
744
745    // Write to file if requested
746    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
762/// Run comprehensive analysis with live cluster data and output unified JSON report.
763fn 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    // Run static analysis with default config
777    let static_config = K8sOptimizeConfig::default();
778    let resource_result = analyze(path, &static_config);
779
780    // Run kubelint with default config
781    let kubelint_config = KubelintConfig::default().with_all_builtin();
782    let kubelint_result = kubelint(path, &kubelint_config);
783
784    // Run helmlint on all charts
785    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    // Build live cluster summary with P95 indicator
811    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    // Deduplicate live vs static findings
831    // Live findings take precedence but static findings that match increase confidence
832    let (deduplicated_recs, dedup_stats) = deduplicate_recommendations(
833        &live_result.recommendations,
834        &resource_result.recommendations,
835    );
836
837    // Calculate totals using deduplicated data
838    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    // Count issues from all sources (using deduplicated count)
843    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    // Log deduplication stats
849    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    // Count severities
857    let mut critical = 0usize;
858    let mut high = 0usize;
859    let mut medium = 0usize;
860
861    // Count from live recommendations
862    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    // Count from static recommendations
872    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    // Count from security findings
882    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    // Use live confidence when available, otherwise calculate
891    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    // Build unified report
901    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            // Parse cloud provider from options
979            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    // Output JSON
1005    println!(
1006        "{}",
1007        serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
1008    );
1009
1010    Ok(())
1011}
1012
1013/// Run live-only unified report (when no path is provided).
1014fn 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, // Not tracked in live-only mode
1037    };
1038
1039    // Count severities from live recommendations
1040    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, // No cloud provider info in live-only mode
1115        precise_fixes: None,   // No static files in live-only mode
1116        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
1132/// Statistics about deduplication.
1133struct DeduplicationStats {
1134    duplicates_removed: usize,
1135    corroborated: usize,
1136}
1137
1138/// Merged recommendation from live and/or static sources.
1139#[derive(Debug, Clone)]
1140#[allow(dead_code)] // Used for deduplication tracking
1141struct MergedRecommendation {
1142    namespace: String,
1143    workload_name: String,
1144    container_name: String,
1145    severity: crate::analyzer::k8s_optimize::Severity,
1146    /// Confidence adjusted for corroboration
1147    confidence: u8,
1148    /// Source of the finding
1149    source: RecommendationSource,
1150    /// CPU waste percentage
1151    cpu_waste_pct: f32,
1152    /// Memory waste percentage
1153    memory_waste_pct: f32,
1154}
1155
1156#[derive(Debug, Clone, PartialEq)]
1157enum RecommendationSource {
1158    LiveOnly,
1159    StaticOnly,
1160    Corroborated,
1161}
1162
1163/// Deduplicate live vs static recommendations.
1164/// Live findings take precedence, but matching static findings increase confidence.
1165fn 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    // First, add all live recommendations (highest priority)
1178    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    // Then check static recommendations
1200    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            // Live finding exists - this is corroborated
1209            // Boost confidence by 10% (up to 100)
1210            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            // Only static finding exists
1216            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, // Lower confidence for static-only
1224                    source: RecommendationSource::StaticOnly,
1225                    cpu_waste_pct: 0.0, // Static analysis doesn't have precise waste metrics
1226                    memory_waste_pct: 0.0,
1227                },
1228            );
1229        }
1230    }
1231
1232    (merged.into_values().collect(), stats)
1233}
1234
1235/// Format millicores to human-readable string.
1236fn 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
1244/// Format bytes to human-readable string.
1245fn 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        // Should not panic, just return empty results
1269        assert!(result.is_ok());
1270    }
1271}