1use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use super::live_analyzer::LiveRecommendation;
11use super::types::{
12 FixApplicationResult, FixImpact, FixResourceValues, FixRisk, FixSource, FixStatus, PreciseFix,
13 ResourceRecommendation, Severity,
14};
15
16#[derive(Debug, Clone)]
18pub struct YamlLocation {
19 pub start_line: u32,
21 pub resources_line: Option<u32>,
23 pub resources_column: Option<u32>,
25 pub yaml_path: String,
27}
28
29pub fn locate_resources_in_file(
31 file_path: &Path,
32 recommendations: &[LiveRecommendation],
33) -> Vec<PreciseFix> {
34 let content = match fs::read_to_string(file_path) {
35 Ok(c) => c,
36 Err(_) => return vec![],
37 };
38
39 let mut fixes = Vec::new();
40
41 for doc in yaml_rust2::YamlLoader::load_from_str(&content).unwrap_or_default() {
43 let locations = find_workload_locations(&content, &doc);
45
46 for rec in recommendations {
47 if let Some(loc) =
48 locations.get(&(rec.workload_name.clone(), rec.container_name.clone()))
49 {
50 let fix = create_precise_fix(file_path, rec, loc);
51 fixes.push(fix);
52 }
53 }
54 }
55
56 fixes
57}
58
59pub fn locate_resources_from_static(recommendations: &[ResourceRecommendation]) -> Vec<PreciseFix> {
61 let mut fixes = Vec::new();
62
63 for rec in recommendations {
64 let fix = PreciseFix {
66 id: generate_fix_id(&rec.resource_name, &rec.container),
67 file_path: rec.file_path.clone(),
68 line_number: rec.line.unwrap_or(0),
69 column: None,
70 resource_kind: rec.resource_kind.clone(),
71 resource_name: rec.resource_name.clone(),
72 container_name: rec.container.clone(),
73 namespace: rec.namespace.clone(),
74 current: FixResourceValues {
75 cpu_request: rec.current.cpu_request.clone(),
76 cpu_limit: rec.current.cpu_limit.clone(),
77 memory_request: rec.current.memory_request.clone(),
78 memory_limit: rec.current.memory_limit.clone(),
79 },
80 recommended: FixResourceValues {
81 cpu_request: rec.recommended.cpu_request.clone(),
82 cpu_limit: rec.recommended.cpu_limit.clone(),
83 memory_request: rec.recommended.memory_request.clone(),
84 memory_limit: rec.recommended.memory_limit.clone(),
85 },
86 confidence: severity_to_confidence(&rec.severity),
87 source: FixSource::StaticAnalysis,
88 impact: assess_impact(rec),
89 status: FixStatus::Pending,
90 };
91 fixes.push(fix);
92 }
93
94 fixes
95}
96
97fn find_workload_locations(
99 content: &str,
100 _doc: &yaml_rust2::Yaml,
101) -> HashMap<(String, String), YamlLocation> {
102 let mut locations = HashMap::new();
103
104 let lines: Vec<&str> = content.lines().collect();
105 let mut current_kind = String::new();
106 let mut current_name = String::new();
107 let mut current_container = String::new();
108 let mut workload_start_line: u32 = 0;
109 let mut in_containers = false;
110 let mut resources_line: Option<u32> = None;
111
112 for (idx, line) in lines.iter().enumerate() {
113 let line_num = (idx + 1) as u32;
114 let trimmed = line.trim();
115
116 if trimmed.starts_with("kind:") {
118 current_kind = trimmed.trim_start_matches("kind:").trim().to_string();
119 workload_start_line = line_num;
120 current_name.clear();
121 current_container.clear();
122 in_containers = false;
123 resources_line = None;
124 }
125
126 if trimmed.starts_with("name:") && !in_containers {
128 current_name = trimmed.trim_start_matches("name:").trim().to_string();
129 }
130
131 if trimmed == "containers:" {
133 in_containers = true;
134 }
135
136 if in_containers && trimmed.starts_with("- name:") {
138 current_container = trimmed.trim_start_matches("- name:").trim().to_string();
139 }
140
141 if in_containers && trimmed == "resources:" {
143 resources_line = Some(line_num);
144
145 if !current_name.is_empty() && !current_container.is_empty() {
147 let key = (current_name.clone(), current_container.clone());
148 locations.insert(
149 key,
150 YamlLocation {
151 start_line: workload_start_line,
152 resources_line,
153 resources_column: Some(line.len() as u32 - trimmed.len() as u32),
154 yaml_path: format!(
155 "{}/{}/containers/{}/resources",
156 current_kind, current_name, current_container
157 ),
158 },
159 );
160 }
161 }
162 }
163
164 locations
165}
166
167fn create_precise_fix(
169 file_path: &Path,
170 rec: &LiveRecommendation,
171 loc: &YamlLocation,
172) -> PreciseFix {
173 let cpu_str = format_millicores(rec.recommended_cpu_millicores);
174 let mem_str = format_bytes(rec.recommended_memory_bytes);
175
176 let current_cpu = rec.current_cpu_millicores.map(format_millicores);
178 let current_mem = rec.current_memory_bytes.map(format_bytes);
179
180 PreciseFix {
181 id: generate_fix_id(&rec.workload_name, &rec.container_name),
182 file_path: file_path.to_path_buf(),
183 line_number: loc.resources_line.unwrap_or(loc.start_line),
184 column: loc.resources_column,
185 resource_kind: rec.workload_kind.clone(),
186 resource_name: rec.workload_name.clone(),
187 container_name: rec.container_name.clone(),
188 namespace: Some(rec.namespace.clone()),
189 current: FixResourceValues {
190 cpu_request: current_cpu.clone(),
191 cpu_limit: current_cpu.map(|c| double_millicores(&c)),
192 memory_request: current_mem.clone(),
193 memory_limit: current_mem.clone(),
194 },
195 recommended: FixResourceValues {
196 cpu_request: Some(cpu_str.clone()),
197 cpu_limit: Some(double_millicores(&cpu_str)),
198 memory_request: Some(mem_str.clone()),
199 memory_limit: Some(mem_str),
200 },
201 confidence: rec.confidence,
202 source: match rec.data_source {
203 super::live_analyzer::DataSource::Prometheus => FixSource::PrometheusP95,
204 super::live_analyzer::DataSource::MetricsServer => FixSource::MetricsServer,
205 super::live_analyzer::DataSource::Combined => FixSource::Combined,
206 super::live_analyzer::DataSource::Static => FixSource::StaticAnalysis,
207 },
208 impact: FixImpact {
209 risk: if rec.confidence >= 80 {
210 FixRisk::Low
211 } else if rec.confidence >= 60 {
212 FixRisk::Medium
213 } else {
214 FixRisk::High
215 },
216 monthly_savings: 0.0, oom_risk: rec.memory_waste_pct < -10.0, throttle_risk: rec.cpu_waste_pct < -10.0, recommendation: if rec.confidence >= 80 {
220 "Safe to apply - high confidence based on observed usage".to_string()
221 } else if rec.confidence >= 60 {
222 "Review before applying - moderate confidence".to_string()
223 } else {
224 "Manual review required - limited data available".to_string()
225 },
226 },
227 status: FixStatus::Pending,
228 }
229}
230
231pub fn apply_fixes(
233 fixes: &mut [PreciseFix],
234 backup_dir: Option<&Path>,
235 dry_run: bool,
236 min_confidence: u8,
237) -> FixApplicationResult {
238 let mut applied = 0;
239 let mut skipped = 0;
240 let mut failed = 0;
241 let mut errors = Vec::new();
242
243 let backup_path = if !dry_run {
245 if let Some(dir) = backup_dir {
246 match fs::create_dir_all(dir) {
247 Ok(_) => Some(dir.to_path_buf()),
248 Err(e) => {
249 errors.push(format!("Failed to create backup dir: {}", e));
250 None
251 }
252 }
253 } else {
254 None
255 }
256 } else {
257 None
258 };
259
260 let mut fixes_by_file: HashMap<PathBuf, Vec<&mut PreciseFix>> = HashMap::new();
262 for fix in fixes.iter_mut() {
263 fixes_by_file
264 .entry(fix.file_path.clone())
265 .or_default()
266 .push(fix);
267 }
268
269 for (file_path, file_fixes) in fixes_by_file.iter_mut() {
271 let content = match fs::read_to_string(file_path) {
273 Ok(c) => c,
274 Err(e) => {
275 errors.push(format!("Failed to read {}: {}", file_path.display(), e));
276 for fix in file_fixes.iter_mut() {
277 fix.status = FixStatus::Failed;
278 failed += 1;
279 }
280 continue;
281 }
282 };
283
284 if !dry_run && let Some(ref backup) = backup_path {
286 let backup_file = backup.join(file_path.file_name().unwrap_or_default());
287 if let Err(e) = fs::write(&backup_file, &content) {
288 errors.push(format!("Failed to backup {}: {}", file_path.display(), e));
289 }
290 }
291
292 let mut modified_content = content.clone();
293 let mut line_offset: i32 = 0;
294
295 file_fixes.sort_by(|a, b| b.line_number.cmp(&a.line_number));
297
298 for fix in file_fixes.iter_mut() {
299 if fix.confidence < min_confidence {
301 fix.status = FixStatus::Skipped;
302 skipped += 1;
303 continue;
304 }
305
306 if fix.impact.risk == FixRisk::Critical {
308 fix.status = FixStatus::Skipped;
309 skipped += 1;
310 continue;
311 }
312
313 match apply_single_fix(&mut modified_content, fix, &mut line_offset) {
315 Ok(_) => {
316 fix.status = if dry_run {
317 FixStatus::Pending
318 } else {
319 FixStatus::Applied
320 };
321 applied += 1;
322 }
323 Err(e) => {
324 fix.status = FixStatus::Failed;
325 errors.push(format!("Fix {} failed: {}", fix.id, e));
326 failed += 1;
327 }
328 }
329 }
330
331 if !dry_run
333 && applied > 0
334 && let Err(e) = fs::write(file_path, &modified_content)
335 {
336 errors.push(format!("Failed to write {}: {}", file_path.display(), e));
337 }
338 }
339
340 FixApplicationResult {
341 total_fixes: fixes.len(),
342 applied,
343 skipped,
344 failed,
345 backup_path,
346 fixes: fixes.to_vec(),
347 errors,
348 }
349}
350
351fn apply_single_fix(
353 content: &mut String,
354 fix: &PreciseFix,
355 _line_offset: &mut i32,
356) -> Result<(), String> {
357 let lines: Vec<&str> = content.lines().collect();
358
359 let target_line = fix.line_number as usize;
361
362 if target_line == 0 || target_line > lines.len() {
363 return Err(format!("Invalid line number: {}", target_line));
364 }
365
366 let indent = detect_indent(&lines, target_line - 1);
368 let new_resources = generate_resources_yaml(fix, &indent);
369
370 let (start_idx, end_idx) = find_resources_section(&lines, target_line - 1)?;
372
373 let mut new_lines: Vec<String> = Vec::new();
375 new_lines.extend(lines[..start_idx].iter().map(|s| s.to_string()));
376 new_lines.push(new_resources);
377 new_lines.extend(lines[end_idx..].iter().map(|s| s.to_string()));
378
379 *content = new_lines.join("\n");
380
381 Ok(())
382}
383
384fn find_resources_section(lines: &[&str], start: usize) -> Result<(usize, usize), String> {
386 let base_indent = lines
387 .get(start)
388 .map(|l| l.len() - l.trim_start().len())
389 .unwrap_or(0);
390
391 let mut end = start + 1;
393 while end < lines.len() {
394 let line = lines[end];
395 let trimmed = line.trim_start();
396
397 if trimmed.is_empty() {
399 end += 1;
400 continue;
401 }
402
403 let current_indent = line.len() - trimmed.len();
404
405 if current_indent <= base_indent && !trimmed.starts_with('-') {
407 break;
408 }
409
410 end += 1;
411 }
412
413 Ok((start, end))
414}
415
416fn detect_indent(lines: &[&str], line_idx: usize) -> String {
418 lines
419 .get(line_idx)
420 .map(|l| {
421 let trimmed = l.trim_start();
422 let indent_len = l.len() - trimmed.len();
423 " ".repeat(indent_len)
424 })
425 .unwrap_or_else(|| " ".to_string()) }
427
428fn generate_resources_yaml(fix: &PreciseFix, indent: &str) -> String {
430 let child_indent = format!("{} ", indent);
431
432 let mut yaml = format!("{}resources:\n", indent);
433 yaml.push_str(&format!("{}requests:\n", child_indent));
434
435 if let Some(ref cpu) = fix.recommended.cpu_request {
436 yaml.push_str(&format!("{} cpu: \"{}\"\n", child_indent, cpu));
437 }
438 if let Some(ref mem) = fix.recommended.memory_request {
439 yaml.push_str(&format!("{} memory: \"{}\"\n", child_indent, mem));
440 }
441
442 yaml.push_str(&format!("{}limits:\n", child_indent));
443
444 if let Some(ref cpu) = fix.recommended.cpu_limit {
445 yaml.push_str(&format!("{} cpu: \"{}\"\n", child_indent, cpu));
446 }
447 if let Some(ref mem) = fix.recommended.memory_limit {
448 yaml.push_str(&format!("{} memory: \"{}\"", child_indent, mem));
449 }
450
451 yaml
452}
453
454fn generate_fix_id(workload: &str, container: &str) -> String {
456 use std::time::{SystemTime, UNIX_EPOCH};
457 let ts = SystemTime::now()
458 .duration_since(UNIX_EPOCH)
459 .map(|d| d.as_millis())
460 .unwrap_or(0);
461 format!("fix-{}-{}-{}", workload, container, ts % 10000)
462}
463
464fn severity_to_confidence(severity: &Severity) -> u8 {
466 match severity {
467 Severity::Critical => 95,
468 Severity::High => 80,
469 Severity::Medium => 60,
470 Severity::Low => 40,
471 Severity::Info => 20,
472 }
473}
474
475fn assess_impact(rec: &ResourceRecommendation) -> FixImpact {
477 let risk = match rec.severity {
478 Severity::Critical | Severity::High => FixRisk::High,
479 Severity::Medium => FixRisk::Medium,
480 _ => FixRisk::Low,
481 };
482
483 FixImpact {
484 risk,
485 monthly_savings: 0.0,
486 oom_risk: false,
487 throttle_risk: false,
488 recommendation: rec.message.clone(),
489 }
490}
491
492fn format_millicores(millicores: u64) -> String {
494 if millicores >= 1000 && millicores.is_multiple_of(1000) {
495 format!("{}", millicores / 1000)
496 } else {
497 format!("{}m", millicores)
498 }
499}
500
501fn double_millicores(value: &str) -> String {
503 if value.ends_with('m') {
504 let m: u64 = value.trim_end_matches('m').parse().unwrap_or(100);
505 format!("{}m", m * 2)
506 } else {
507 let cores: f64 = value.parse().unwrap_or(0.5);
508 format!("{}", cores * 2.0)
509 }
510}
511
512fn format_bytes(bytes: u64) -> String {
514 if bytes >= 1024 * 1024 * 1024 && bytes.is_multiple_of(1024 * 1024 * 1024) {
515 format!("{}Gi", bytes / (1024 * 1024 * 1024))
516 } else if bytes >= 1024 * 1024 {
517 format!("{}Mi", bytes / (1024 * 1024))
518 } else if bytes >= 1024 {
519 format!("{}Ki", bytes / 1024)
520 } else {
521 format!("{}", bytes)
522 }
523}