syncable_cli/analyzer/k8s_optimize/parser/
yaml.rs1use crate::analyzer::k8s_optimize::types::{ResourceSpec, WorkloadType};
7use regex::Regex;
8use std::sync::LazyLock;
9
10static CPU_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\d+(?:\.\d+)?)(m)?$").unwrap());
16
17pub 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
44pub 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
58static 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
66pub 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 "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
107pub 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 format!("{}Mi", bytes / MI)
129 } else {
130 format!("{}", bytes)
131 }
132}
133
134pub 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
164pub fn extract_container_name(container: &serde_yaml::Value) -> Option<String> {
166 container.get("name")?.as_str().map(String::from)
167}
168
169pub fn extract_container_image(container: &serde_yaml::Value) -> Option<String> {
171 container.get("image")?.as_str().map(String::from)
172}
173
174pub 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 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 if *db == "redis" || *db == "memcached" {
207 return WorkloadType::Cache;
208 }
209 return WorkloadType::Database;
210 }
211 }
212
213 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 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 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 if kind == "Job" || kind == "CronJob" {
261 return WorkloadType::Batch;
262 }
263
264 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 WorkloadType::General
277}
278
279pub 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
295pub 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#[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}