syncable_cli/analyzer/k8s_optimize/
cost_calculator.rs

1//! Cost Calculator for Kubernetes Resource Waste
2//!
3//! Estimates the cost of wasted resources based on cloud provider pricing.
4//! Supports AWS, GCP, Azure, and on-prem estimates.
5
6use super::live_analyzer::LiveRecommendation;
7use super::types::{
8    CloudProvider, CostBreakdown, CostEstimation, ResourceRecommendation, WorkloadCost,
9};
10
11/// Default pricing per vCPU-hour (in USD)
12const AWS_CPU_HOURLY: f64 = 0.0416; // ~$30/month per vCPU (on-demand m5.large)
13const GCP_CPU_HOURLY: f64 = 0.0335; // ~$24/month per vCPU (n1-standard)
14const AZURE_CPU_HOURLY: f64 = 0.0400; // ~$29/month per vCPU (D2s v3)
15const ONPREM_CPU_HOURLY: f64 = 0.0250; // ~$18/month per vCPU (rough estimate)
16
17/// Default pricing per GB-hour (in USD)
18const AWS_MEM_HOURLY: f64 = 0.0052; // ~$3.75/month per GB
19const GCP_MEM_HOURLY: f64 = 0.0045; // ~$3.24/month per GB
20const AZURE_MEM_HOURLY: f64 = 0.0050; // ~$3.60/month per GB
21const ONPREM_MEM_HOURLY: f64 = 0.0030; // ~$2.16/month per GB (rough estimate)
22
23/// Hours in a month (for cost calculations)
24const HOURS_PER_MONTH: f64 = 730.0;
25
26/// Calculate cost estimation from live analysis results.
27pub fn calculate_from_live(
28    recommendations: &[LiveRecommendation],
29    provider: CloudProvider,
30    region: &str,
31) -> CostEstimation {
32    let (cpu_hourly, mem_hourly) = get_pricing(&provider);
33
34    let mut total_cpu_waste_millicores: u64 = 0;
35    let mut total_memory_waste_bytes: u64 = 0;
36    let mut workload_costs: Vec<WorkloadCost> = Vec::new();
37
38    for rec in recommendations {
39        // Calculate waste (only for over-provisioned resources)
40        let cpu_waste = if rec.cpu_waste_pct > 0.0 {
41            // Current CPU minus recommended = waste
42            let current = rec.current_cpu_millicores.unwrap_or(0);
43            current.saturating_sub(rec.recommended_cpu_millicores)
44        } else {
45            0
46        };
47
48        let memory_waste = if rec.memory_waste_pct > 0.0 {
49            let current = rec.current_memory_bytes.unwrap_or(0);
50            current.saturating_sub(rec.recommended_memory_bytes)
51        } else {
52            0
53        };
54
55        total_cpu_waste_millicores += cpu_waste;
56        total_memory_waste_bytes += memory_waste;
57
58        // Calculate per-workload cost
59        let cpu_cores = cpu_waste as f64 / 1000.0;
60        let memory_gb = memory_waste as f64 / (1024.0 * 1024.0 * 1024.0);
61
62        let monthly_cost =
63            (cpu_cores * cpu_hourly * HOURS_PER_MONTH) + (memory_gb * mem_hourly * HOURS_PER_MONTH);
64
65        if monthly_cost > 0.01 {
66            workload_costs.push(WorkloadCost {
67                namespace: rec.namespace.clone(),
68                workload_name: rec.workload_name.clone(),
69                monthly_cost: round_cost(monthly_cost),
70                monthly_savings: round_cost(monthly_cost),
71            });
72        }
73    }
74
75    // Calculate totals
76    let cpu_cores = total_cpu_waste_millicores as f64 / 1000.0;
77    let memory_gb = total_memory_waste_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
78
79    let cpu_monthly = cpu_cores * cpu_hourly * HOURS_PER_MONTH;
80    let mem_monthly = memory_gb * mem_hourly * HOURS_PER_MONTH;
81    let monthly_waste = cpu_monthly + mem_monthly;
82
83    // Sort workloads by cost (highest first)
84    workload_costs.sort_by(|a, b| {
85        b.monthly_cost
86            .partial_cmp(&a.monthly_cost)
87            .unwrap_or(std::cmp::Ordering::Equal)
88    });
89
90    CostEstimation {
91        provider,
92        region: region.to_string(),
93        monthly_waste_cost: round_cost(monthly_waste),
94        annual_waste_cost: round_cost(monthly_waste * 12.0),
95        monthly_savings: round_cost(monthly_waste),
96        annual_savings: round_cost(monthly_waste * 12.0),
97        currency: "USD".to_string(),
98        breakdown: CostBreakdown {
99            cpu_cost: round_cost(cpu_monthly),
100            memory_cost: round_cost(mem_monthly),
101        },
102        workload_costs,
103    }
104}
105
106/// Calculate cost estimation from static analysis results.
107pub fn calculate_from_static(
108    recommendations: &[ResourceRecommendation],
109    provider: CloudProvider,
110    region: &str,
111) -> CostEstimation {
112    let (cpu_hourly, mem_hourly) = get_pricing(&provider);
113
114    let mut total_cpu_waste_millicores: u64 = 0;
115    let mut total_memory_waste_bytes: u64 = 0;
116    let mut workload_costs: Vec<WorkloadCost> = Vec::new();
117
118    for rec in recommendations {
119        // For static analysis, estimate waste from current vs recommended
120        let cpu_waste = parse_cpu_to_millicores(&rec.current.cpu_request)
121            .saturating_sub(parse_cpu_to_millicores(&rec.recommended.cpu_request));
122
123        let memory_waste = parse_memory_to_bytes(&rec.current.memory_request)
124            .saturating_sub(parse_memory_to_bytes(&rec.recommended.memory_request));
125
126        total_cpu_waste_millicores += cpu_waste;
127        total_memory_waste_bytes += memory_waste;
128
129        let cpu_cores = cpu_waste as f64 / 1000.0;
130        let memory_gb = memory_waste as f64 / (1024.0 * 1024.0 * 1024.0);
131
132        let monthly_cost =
133            (cpu_cores * cpu_hourly * HOURS_PER_MONTH) + (memory_gb * mem_hourly * HOURS_PER_MONTH);
134
135        if monthly_cost > 0.01 {
136            workload_costs.push(WorkloadCost {
137                namespace: rec
138                    .namespace
139                    .clone()
140                    .unwrap_or_else(|| "default".to_string()),
141                workload_name: rec.resource_name.clone(),
142                monthly_cost: round_cost(monthly_cost),
143                monthly_savings: round_cost(monthly_cost),
144            });
145        }
146    }
147
148    let cpu_cores = total_cpu_waste_millicores as f64 / 1000.0;
149    let memory_gb = total_memory_waste_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
150
151    let cpu_monthly = cpu_cores * cpu_hourly * HOURS_PER_MONTH;
152    let mem_monthly = memory_gb * mem_hourly * HOURS_PER_MONTH;
153    let monthly_waste = cpu_monthly + mem_monthly;
154
155    workload_costs.sort_by(|a, b| {
156        b.monthly_cost
157            .partial_cmp(&a.monthly_cost)
158            .unwrap_or(std::cmp::Ordering::Equal)
159    });
160
161    CostEstimation {
162        provider,
163        region: region.to_string(),
164        monthly_waste_cost: round_cost(monthly_waste),
165        annual_waste_cost: round_cost(monthly_waste * 12.0),
166        monthly_savings: round_cost(monthly_waste),
167        annual_savings: round_cost(monthly_waste * 12.0),
168        currency: "USD".to_string(),
169        breakdown: CostBreakdown {
170            cpu_cost: round_cost(cpu_monthly),
171            memory_cost: round_cost(mem_monthly),
172        },
173        workload_costs,
174    }
175}
176
177/// Get pricing for a cloud provider.
178fn get_pricing(provider: &CloudProvider) -> (f64, f64) {
179    match provider {
180        CloudProvider::Aws => (AWS_CPU_HOURLY, AWS_MEM_HOURLY),
181        CloudProvider::Gcp => (GCP_CPU_HOURLY, GCP_MEM_HOURLY),
182        CloudProvider::Azure => (AZURE_CPU_HOURLY, AZURE_MEM_HOURLY),
183        CloudProvider::OnPrem => (ONPREM_CPU_HOURLY, ONPREM_MEM_HOURLY),
184        CloudProvider::Unknown => (AWS_CPU_HOURLY, AWS_MEM_HOURLY), // Default to AWS
185    }
186}
187
188/// Parse CPU string (e.g., "100m", "1.5") to millicores.
189fn parse_cpu_to_millicores(cpu: &Option<String>) -> u64 {
190    let cpu_str = match cpu {
191        Some(s) => s,
192        None => return 0,
193    };
194
195    if cpu_str.ends_with('m') {
196        cpu_str.trim_end_matches('m').parse().unwrap_or(0)
197    } else {
198        // Full cores
199        let cores: f64 = cpu_str.parse().unwrap_or(0.0);
200        (cores * 1000.0) as u64
201    }
202}
203
204/// Parse memory string (e.g., "128Mi", "1Gi") to bytes.
205fn parse_memory_to_bytes(memory: &Option<String>) -> u64 {
206    let mem_str = match memory {
207        Some(s) => s,
208        None => return 0,
209    };
210
211    let mem_str = mem_str.trim();
212
213    if mem_str.ends_with("Gi") {
214        let val: f64 = mem_str.trim_end_matches("Gi").parse().unwrap_or(0.0);
215        (val * 1024.0 * 1024.0 * 1024.0) as u64
216    } else if mem_str.ends_with("Mi") {
217        let val: f64 = mem_str.trim_end_matches("Mi").parse().unwrap_or(0.0);
218        (val * 1024.0 * 1024.0) as u64
219    } else if mem_str.ends_with("Ki") {
220        let val: f64 = mem_str.trim_end_matches("Ki").parse().unwrap_or(0.0);
221        (val * 1024.0) as u64
222    } else {
223        // Assume bytes
224        mem_str.parse().unwrap_or(0)
225    }
226}
227
228/// Round cost to 2 decimal places.
229fn round_cost(cost: f64) -> f64 {
230    (cost * 100.0).round() / 100.0
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_parse_cpu() {
239        assert_eq!(parse_cpu_to_millicores(&Some("100m".to_string())), 100);
240        assert_eq!(parse_cpu_to_millicores(&Some("1".to_string())), 1000);
241        assert_eq!(parse_cpu_to_millicores(&Some("1.5".to_string())), 1500);
242        assert_eq!(parse_cpu_to_millicores(&None), 0);
243    }
244
245    #[test]
246    fn test_parse_memory() {
247        assert_eq!(
248            parse_memory_to_bytes(&Some("128Mi".to_string())),
249            128 * 1024 * 1024
250        );
251        assert_eq!(
252            parse_memory_to_bytes(&Some("1Gi".to_string())),
253            1024 * 1024 * 1024
254        );
255        assert_eq!(parse_memory_to_bytes(&None), 0);
256    }
257
258    #[test]
259    fn test_round_cost() {
260        assert_eq!(round_cost(10.1234), 10.12);
261        assert_eq!(round_cost(10.125), 10.13);
262    }
263}