Skip to main content

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, format_result_to_string,
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 optimize in agent mode - returns JSON string without printing.
99pub 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
129/// Handle static analysis (Phase 1) - analyzes manifests without cluster connection.
130fn handle_static_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
131    // Build config
132    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    // Run resource optimization analysis
157    let result = analyze(path, &config);
158
159    // Determine output format
160    let format = OutputFormat::parse(&options.format).unwrap_or(OutputFormat::Table);
161    let is_json = options.format == "json";
162
163    // If using --full with JSON, skip individual output and only show unified report
164    let skip_individual_output = options.full && is_json;
165
166    // Output resource optimization result (unless skipping for unified JSON)
167    if !skip_individual_output {
168        if let Some(output_path) = &options.output {
169            // Write to file
170            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            // Print to stdout
176            format_result(&result, format);
177        }
178    }
179
180    // Run comprehensive analysis if --full flag is set
181    if options.full {
182        run_comprehensive_analysis(path, &result, is_json)?;
183    }
184
185    // Generate fixes if requested
186    if options.fix {
187        generate_fixes(&result, path)?;
188    }
189
190    // Exit with non-zero if critical issues found
191    if result.summary.missing_requests > 0 || result.summary.over_provisioned > 0 {
192        // We could exit with error here for CI/CD
193        // std::process::exit(1);
194    }
195
196    Ok(())
197}
198
199/// Run comprehensive analysis with kubelint and helmlint.
200fn 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    // Run kubelint
213    let kubelint_config = KubelintConfig::default().with_all_builtin();
214    let kubelint_result = kubelint(path, &kubelint_config);
215
216    // Run helmlint on all charts
217    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, build unified report and print
243    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, // Static analysis confidence
277                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, // No live data in static-only analysis
327            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    // Table output (existing code)
346    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        // Group by priority
362        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        // Show critical issues
392        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        // Show warnings (limited)
407        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    // Helm chart validation output
424    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
461/// Calculate an overall health score based on all findings.
462fn 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    // Start with resource optimization score (40% weight)
471    let resource_score = (optimal_resources / total_resources) * 40.0;
472
473    // Security score (40% weight)
474    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    // Helm validation score (20% weight)
480    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
490/// Find Helm charts in a directory.
491fn 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                    // Check one level deeper
507                    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
523/// Generate optimized manifest files.
524fn 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
551/// Handle live cluster analysis (Phase 2) - connects to cluster for real metrics.
552async fn handle_live_optimize(path: &Path, options: OptimizeOptions) -> Result<()> {
553    use colored::Colorize;
554
555    // Install rustls crypto provider (required for TLS connections to K8s API)
556    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    // Build live analyzer config
569    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    // Create analyzer (with context or default)
580    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    // Check available data sources
593    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    // Run analysis
610    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    // Display results (only in non-JSON mode)
618    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        // Summary
632        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        // Waste summary
659        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        // Recommendations
675        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                // Show recommended values
709                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        // Warnings
729        for warning in &result.warnings {
730            println!("\n\u{26A0}\u{FE0F}  {}", warning.yellow());
731        }
732    }
733
734    // Also run static analysis on manifests if path provided
735    if path.exists() && path.is_dir() {
736        if options.full && is_json {
737            // Run comprehensive analysis and output unified JSON with live data
738            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        // Output live-only unified report
773        run_live_only_unified_report(&result)?;
774    }
775
776    // Write to file if requested
777    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
793/// Run comprehensive analysis with live cluster data and output unified JSON report.
794fn 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    // Run static analysis with default config
808    let static_config = K8sOptimizeConfig::default();
809    let resource_result = analyze(path, &static_config);
810
811    // Run kubelint with default config
812    let kubelint_config = KubelintConfig::default().with_all_builtin();
813    let kubelint_result = kubelint(path, &kubelint_config);
814
815    // Run helmlint on all charts
816    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    // Build live cluster summary with P95 indicator
842    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    // Deduplicate live vs static findings
862    // Live findings take precedence but static findings that match increase confidence
863    let (deduplicated_recs, dedup_stats) = deduplicate_recommendations(
864        &live_result.recommendations,
865        &resource_result.recommendations,
866    );
867
868    // Calculate totals using deduplicated data
869    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    // Count issues from all sources (using deduplicated count)
874    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    // Log deduplication stats
880    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    // Count severities
888    let mut critical = 0usize;
889    let mut high = 0usize;
890    let mut medium = 0usize;
891
892    // Count from live recommendations
893    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    // Count from static recommendations
903    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    // Count from security findings
913    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    // Use live confidence when available, otherwise calculate
922    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    // Build unified report
932    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            // Parse cloud provider from options
1010            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    // Output JSON
1036    println!(
1037        "{}",
1038        serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
1039    );
1040
1041    Ok(())
1042}
1043
1044/// Run live-only unified report (when no path is provided).
1045fn 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, // Not tracked in live-only mode
1068    };
1069
1070    // Count severities from live recommendations
1071    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, // No cloud provider info in live-only mode
1146        precise_fixes: None,   // No static files in live-only mode
1147        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
1163/// Statistics about deduplication.
1164struct DeduplicationStats {
1165    duplicates_removed: usize,
1166    corroborated: usize,
1167}
1168
1169/// Merged recommendation from live and/or static sources.
1170#[derive(Debug, Clone)]
1171#[allow(dead_code)] // Used for deduplication tracking
1172struct MergedRecommendation {
1173    namespace: String,
1174    workload_name: String,
1175    container_name: String,
1176    severity: crate::analyzer::k8s_optimize::Severity,
1177    /// Confidence adjusted for corroboration
1178    confidence: u8,
1179    /// Source of the finding
1180    source: RecommendationSource,
1181    /// CPU waste percentage
1182    cpu_waste_pct: f32,
1183    /// Memory waste percentage
1184    memory_waste_pct: f32,
1185}
1186
1187#[derive(Debug, Clone, PartialEq)]
1188enum RecommendationSource {
1189    LiveOnly,
1190    StaticOnly,
1191    Corroborated,
1192}
1193
1194/// Deduplicate live vs static recommendations.
1195/// Live findings take precedence, but matching static findings increase confidence.
1196fn 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    // First, add all live recommendations (highest priority)
1209    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    // Then check static recommendations
1231    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            // Live finding exists - this is corroborated
1240            // Boost confidence by 10% (up to 100)
1241            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            // Only static finding exists
1247            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, // Lower confidence for static-only
1255                    source: RecommendationSource::StaticOnly,
1256                    cpu_waste_pct: 0.0, // Static analysis doesn't have precise waste metrics
1257                    memory_waste_pct: 0.0,
1258                },
1259            );
1260        }
1261    }
1262
1263    (merged.into_values().collect(), stats)
1264}
1265
1266/// Format millicores to human-readable string.
1267fn 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
1275/// Format bytes to human-readable string.
1276fn 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        // Should not panic, just return empty results
1300        assert!(result.is_ok());
1301    }
1302}