syncable_cli/analyzer/k8s_optimize/parser/
yaml.rs

1//! Resource specification parsing utilities.
2//!
3//! Parses Kubernetes resource values (CPU and memory) from their string
4//! representations to numeric values for comparison and calculation.
5
6use crate::analyzer::k8s_optimize::types::{ResourceSpec, WorkloadType};
7use regex::Regex;
8use std::sync::LazyLock;
9
10// ============================================================================
11// CPU Parsing
12// ============================================================================
13
14/// Regex for parsing CPU values (e.g., "100m", "1", "1.5", "0.1")
15static CPU_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\d+(?:\.\d+)?)(m)?$").unwrap());
16
17/// Parse a CPU value string to millicores.
18///
19/// # Examples
20/// - "100m" -> 100
21/// - "1" -> 1000
22/// - "1.5" -> 1500
23/// - "0.1" -> 100
24pub fn parse_cpu_to_millicores(cpu: &str) -> Option<u64> {
25    let cpu = cpu.trim();
26    if cpu.is_empty() {
27        return None;
28    }
29
30    if let Some(caps) = CPU_REGEX.captures(cpu) {
31        let value: f64 = caps.get(1)?.as_str().parse().ok()?;
32        let is_millicores = caps.get(2).is_some();
33
34        if is_millicores {
35            Some(value as u64)
36        } else {
37            Some((value * 1000.0) as u64)
38        }
39    } else {
40        None
41    }
42}
43
44/// Convert millicores to a human-readable CPU string.
45///
46/// # Examples
47/// - 100 -> "100m"
48/// - 1000 -> "1"
49/// - 1500 -> "1500m"
50pub fn millicores_to_cpu_string(millicores: u64) -> String {
51    if millicores >= 1000 && millicores.is_multiple_of(1000) {
52        format!("{}", millicores / 1000)
53    } else {
54        format!("{}m", millicores)
55    }
56}
57
58// ============================================================================
59// Memory Parsing
60// ============================================================================
61
62/// Regex for parsing memory values (e.g., "128Mi", "1Gi", "1024Ki", "1000000000")
63static MEMORY_REGEX: LazyLock<Regex> =
64    LazyLock::new(|| Regex::new(r"^(\d+(?:\.\d+)?)(Ki|Mi|Gi|Ti|Pi|Ei|K|M|G|T|P|E)?$").unwrap());
65
66/// Parse a memory value string to bytes.
67///
68/// # Examples
69/// - "128Mi" -> 134217728
70/// - "1Gi" -> 1073741824
71/// - "1024Ki" -> 1048576
72/// - "1000000000" -> 1000000000
73pub fn parse_memory_to_bytes(memory: &str) -> Option<u64> {
74    let memory = memory.trim();
75    if memory.is_empty() {
76        return None;
77    }
78
79    if let Some(caps) = MEMORY_REGEX.captures(memory) {
80        let value: f64 = caps.get(1)?.as_str().parse().ok()?;
81        let unit = caps.get(2).map(|m| m.as_str()).unwrap_or("");
82
83        let multiplier: f64 = match unit {
84            "" => 1.0,
85            "Ki" => 1024.0,
86            "Mi" => 1024.0 * 1024.0,
87            "Gi" => 1024.0 * 1024.0 * 1024.0,
88            "Ti" => 1024.0 * 1024.0 * 1024.0 * 1024.0,
89            "Pi" => 1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0,
90            "Ei" => 1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0 * 1024.0,
91            // Decimal units
92            "K" => 1000.0,
93            "M" => 1000.0 * 1000.0,
94            "G" => 1000.0 * 1000.0 * 1000.0,
95            "T" => 1000.0 * 1000.0 * 1000.0 * 1000.0,
96            "P" => 1000.0 * 1000.0 * 1000.0 * 1000.0 * 1000.0,
97            "E" => 1000.0 * 1000.0 * 1000.0 * 1000.0 * 1000.0 * 1000.0,
98            _ => return None,
99        };
100
101        Some((value * multiplier) as u64)
102    } else {
103        None
104    }
105}
106
107/// Convert bytes to a human-readable memory string (using binary units).
108///
109/// # Examples
110/// - 134217728 -> "128Mi"
111/// - 1073741824 -> "1Gi"
112pub fn bytes_to_memory_string(bytes: u64) -> String {
113    const KI: u64 = 1024;
114    const MI: u64 = KI * 1024;
115    const GI: u64 = MI * 1024;
116    const TI: u64 = GI * 1024;
117
118    if bytes >= TI && bytes.is_multiple_of(TI) {
119        format!("{}Ti", bytes / TI)
120    } else if bytes >= GI && bytes.is_multiple_of(GI) {
121        format!("{}Gi", bytes / GI)
122    } else if bytes >= MI && bytes.is_multiple_of(MI) {
123        format!("{}Mi", bytes / MI)
124    } else if bytes >= KI && bytes.is_multiple_of(KI) {
125        format!("{}Ki", bytes / KI)
126    } else if bytes >= MI {
127        // Round to Mi for readability
128        format!("{}Mi", bytes / MI)
129    } else {
130        format!("{}", bytes)
131    }
132}
133
134// ============================================================================
135// Resource Spec Parsing from YAML
136// ============================================================================
137
138/// Extract resources from a container YAML value.
139pub fn extract_resources(container: &serde_yaml::Value) -> ResourceSpec {
140    let mut spec = ResourceSpec::new();
141
142    if let Some(resources) = container.get("resources") {
143        if let Some(requests) = resources.get("requests") {
144            if let Some(cpu) = requests.get("cpu") {
145                spec.cpu_request = cpu.as_str().map(String::from);
146            }
147            if let Some(memory) = requests.get("memory") {
148                spec.memory_request = memory.as_str().map(String::from);
149            }
150        }
151        if let Some(limits) = resources.get("limits") {
152            if let Some(cpu) = limits.get("cpu") {
153                spec.cpu_limit = cpu.as_str().map(String::from);
154            }
155            if let Some(memory) = limits.get("memory") {
156                spec.memory_limit = memory.as_str().map(String::from);
157            }
158        }
159    }
160
161    spec
162}
163
164/// Extract container name from a container YAML value.
165pub fn extract_container_name(container: &serde_yaml::Value) -> Option<String> {
166    container.get("name")?.as_str().map(String::from)
167}
168
169/// Extract image from a container YAML value.
170pub fn extract_container_image(container: &serde_yaml::Value) -> Option<String> {
171    container.get("image")?.as_str().map(String::from)
172}
173
174// ============================================================================
175// Workload Type Detection
176// ============================================================================
177
178/// Detect workload type from container image and name.
179pub fn detect_workload_type(
180    image: Option<&str>,
181    container_name: Option<&str>,
182    kind: &str,
183) -> WorkloadType {
184    let image = image.unwrap_or("").to_lowercase();
185    let name = container_name.unwrap_or("").to_lowercase();
186
187    // Database indicators
188    const DB_IMAGES: &[&str] = &[
189        "postgres",
190        "mysql",
191        "mariadb",
192        "mongodb",
193        "mongo",
194        "redis",
195        "memcached",
196        "elasticsearch",
197        "cassandra",
198        "couchdb",
199        "cockroach",
200        "timescale",
201        "influx",
202    ];
203    for db in DB_IMAGES {
204        if image.contains(db) || name.contains(db) {
205            // Redis and Memcached are caches
206            if *db == "redis" || *db == "memcached" {
207                return WorkloadType::Cache;
208            }
209            return WorkloadType::Database;
210        }
211    }
212
213    // Message broker indicators
214    const BROKER_IMAGES: &[&str] = &["kafka", "rabbitmq", "nats", "pulsar", "activemq", "zeromq"];
215    for broker in BROKER_IMAGES {
216        if image.contains(broker) || name.contains(broker) {
217            return WorkloadType::MessageBroker;
218        }
219    }
220
221    // ML/AI indicators
222    const ML_IMAGES: &[&str] = &[
223        "tensorflow",
224        "pytorch",
225        "nvidia",
226        "cuda",
227        "gpu",
228        "ml",
229        "ai",
230        "jupyter",
231        "notebook",
232        "training",
233    ];
234    for ml in ML_IMAGES {
235        if image.contains(ml) || name.contains(ml) {
236            return WorkloadType::MachineLearning;
237        }
238    }
239
240    // Worker indicators
241    const WORKER_PATTERNS: &[&str] = &[
242        "worker",
243        "consumer",
244        "processor",
245        "handler",
246        "queue",
247        "celery",
248        "sidekiq",
249        "resque",
250        "bull",
251        "bee",
252    ];
253    for pattern in WORKER_PATTERNS {
254        if name.contains(pattern) {
255            return WorkloadType::Worker;
256        }
257    }
258
259    // Job/CronJob kinds are batch
260    if kind == "Job" || kind == "CronJob" {
261        return WorkloadType::Batch;
262    }
263
264    // Web indicators
265    const WEB_IMAGES: &[&str] = &[
266        "nginx", "apache", "httpd", "caddy", "traefik", "envoy", "api", "web", "frontend",
267        "backend", "gateway",
268    ];
269    for web in WEB_IMAGES {
270        if image.contains(web) || name.contains(web) {
271            return WorkloadType::Web;
272        }
273    }
274
275    // Default to general
276    WorkloadType::General
277}
278
279// ============================================================================
280// Ratio Calculations
281// ============================================================================
282
283/// Calculate the limit to request ratio for CPU.
284pub fn cpu_limit_to_request_ratio(spec: &ResourceSpec) -> Option<f64> {
285    let request = parse_cpu_to_millicores(spec.cpu_request.as_deref()?)?;
286    let limit = parse_cpu_to_millicores(spec.cpu_limit.as_deref()?)?;
287
288    if request == 0 {
289        return None;
290    }
291
292    Some(limit as f64 / request as f64)
293}
294
295/// Calculate the limit to request ratio for memory.
296pub fn memory_limit_to_request_ratio(spec: &ResourceSpec) -> Option<f64> {
297    let request = parse_memory_to_bytes(spec.memory_request.as_deref()?)?;
298    let limit = parse_memory_to_bytes(spec.memory_limit.as_deref()?)?;
299
300    if request == 0 {
301        return None;
302    }
303
304    Some(limit as f64 / request as f64)
305}
306
307// ============================================================================
308// Tests
309// ============================================================================
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_parse_cpu_millicores() {
317        assert_eq!(parse_cpu_to_millicores("100m"), Some(100));
318        assert_eq!(parse_cpu_to_millicores("1"), Some(1000));
319        assert_eq!(parse_cpu_to_millicores("1.5"), Some(1500));
320        assert_eq!(parse_cpu_to_millicores("0.1"), Some(100));
321        assert_eq!(parse_cpu_to_millicores("500m"), Some(500));
322        assert_eq!(parse_cpu_to_millicores("2000m"), Some(2000));
323    }
324
325    #[test]
326    fn test_millicores_to_string() {
327        assert_eq!(millicores_to_cpu_string(100), "100m");
328        assert_eq!(millicores_to_cpu_string(1000), "1");
329        assert_eq!(millicores_to_cpu_string(2000), "2");
330        assert_eq!(millicores_to_cpu_string(1500), "1500m");
331    }
332
333    #[test]
334    fn test_parse_memory_bytes() {
335        assert_eq!(parse_memory_to_bytes("128Mi"), Some(128 * 1024 * 1024));
336        assert_eq!(parse_memory_to_bytes("1Gi"), Some(1024 * 1024 * 1024));
337        assert_eq!(parse_memory_to_bytes("1024Ki"), Some(1024 * 1024));
338        assert_eq!(parse_memory_to_bytes("1000000000"), Some(1000000000));
339    }
340
341    #[test]
342    fn test_bytes_to_memory_string() {
343        assert_eq!(bytes_to_memory_string(128 * 1024 * 1024), "128Mi");
344        assert_eq!(bytes_to_memory_string(1024 * 1024 * 1024), "1Gi");
345        assert_eq!(bytes_to_memory_string(1024 * 1024), "1Mi");
346    }
347
348    #[test]
349    fn test_detect_workload_type() {
350        assert_eq!(
351            detect_workload_type(Some("postgres:14"), None, "Deployment"),
352            WorkloadType::Database
353        );
354        assert_eq!(
355            detect_workload_type(Some("redis:7"), None, "Deployment"),
356            WorkloadType::Cache
357        );
358        assert_eq!(
359            detect_workload_type(Some("nginx:latest"), None, "Deployment"),
360            WorkloadType::Web
361        );
362        assert_eq!(
363            detect_workload_type(Some("myapp:v1"), Some("worker"), "Deployment"),
364            WorkloadType::Worker
365        );
366        assert_eq!(
367            detect_workload_type(Some("myapp:v1"), None, "Job"),
368            WorkloadType::Batch
369        );
370        assert_eq!(
371            detect_workload_type(Some("myapp:v1"), None, "Deployment"),
372            WorkloadType::General
373        );
374    }
375
376    #[test]
377    fn test_cpu_ratio() {
378        let spec = ResourceSpec {
379            cpu_request: Some("100m".to_string()),
380            cpu_limit: Some("500m".to_string()),
381            memory_request: None,
382            memory_limit: None,
383        };
384        let ratio = cpu_limit_to_request_ratio(&spec).unwrap();
385        assert!((ratio - 5.0).abs() < 0.01);
386    }
387
388    #[test]
389    fn test_memory_ratio() {
390        let spec = ResourceSpec {
391            cpu_request: None,
392            cpu_limit: None,
393            memory_request: Some("256Mi".to_string()),
394            memory_limit: Some("1Gi".to_string()),
395        };
396        let ratio = memory_limit_to_request_ratio(&spec).unwrap();
397        assert!((ratio - 4.0).abs() < 0.01);
398    }
399
400    #[test]
401    fn test_extract_resources() {
402        let yaml = serde_yaml::from_str::<serde_yaml::Value>(
403            r#"
404            name: nginx
405            image: nginx:1.21
406            resources:
407              requests:
408                cpu: 100m
409                memory: 128Mi
410              limits:
411                cpu: 500m
412                memory: 512Mi
413            "#,
414        )
415        .unwrap();
416
417        let spec = extract_resources(&yaml);
418        assert_eq!(spec.cpu_request, Some("100m".to_string()));
419        assert_eq!(spec.memory_request, Some("128Mi".to_string()));
420        assert_eq!(spec.cpu_limit, Some("500m".to_string()));
421        assert_eq!(spec.memory_limit, Some("512Mi".to_string()));
422    }
423}