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 {
286 if let Some(ref backup) = backup_path {
287 let backup_file = backup.join(file_path.file_name().unwrap_or_default());
288 if let Err(e) = fs::write(&backup_file, &content) {
289 errors.push(format!("Failed to backup {}: {}", file_path.display(), e));
290 }
291 }
292 }
293
294 let mut modified_content = content.clone();
295 let mut line_offset: i32 = 0;
296
297 file_fixes.sort_by(|a, b| b.line_number.cmp(&a.line_number));
299
300 for fix in file_fixes.iter_mut() {
301 if fix.confidence < min_confidence {
303 fix.status = FixStatus::Skipped;
304 skipped += 1;
305 continue;
306 }
307
308 if fix.impact.risk == FixRisk::Critical {
310 fix.status = FixStatus::Skipped;
311 skipped += 1;
312 continue;
313 }
314
315 match apply_single_fix(&mut modified_content, fix, &mut line_offset) {
317 Ok(_) => {
318 fix.status = if dry_run {
319 FixStatus::Pending
320 } else {
321 FixStatus::Applied
322 };
323 applied += 1;
324 }
325 Err(e) => {
326 fix.status = FixStatus::Failed;
327 errors.push(format!("Fix {} failed: {}", fix.id, e));
328 failed += 1;
329 }
330 }
331 }
332
333 if !dry_run && applied > 0 {
335 if let Err(e) = fs::write(file_path, &modified_content) {
336 errors.push(format!("Failed to write {}: {}", file_path.display(), e));
337 }
338 }
339 }
340
341 FixApplicationResult {
342 total_fixes: fixes.len(),
343 applied,
344 skipped,
345 failed,
346 backup_path,
347 fixes: fixes.to_vec(),
348 errors,
349 }
350}
351
352fn apply_single_fix(
354 content: &mut String,
355 fix: &PreciseFix,
356 _line_offset: &mut i32,
357) -> Result<(), String> {
358 let lines: Vec<&str> = content.lines().collect();
359
360 let target_line = fix.line_number as usize;
362
363 if target_line == 0 || target_line > lines.len() {
364 return Err(format!("Invalid line number: {}", target_line));
365 }
366
367 let indent = detect_indent(&lines, target_line - 1);
369 let new_resources = generate_resources_yaml(fix, &indent);
370
371 let (start_idx, end_idx) = find_resources_section(&lines, target_line - 1)?;
373
374 let mut new_lines: Vec<String> = Vec::new();
376 new_lines.extend(lines[..start_idx].iter().map(|s| s.to_string()));
377 new_lines.push(new_resources);
378 new_lines.extend(lines[end_idx..].iter().map(|s| s.to_string()));
379
380 *content = new_lines.join("\n");
381
382 Ok(())
383}
384
385fn find_resources_section(lines: &[&str], start: usize) -> Result<(usize, usize), String> {
387 let base_indent = lines
388 .get(start)
389 .map(|l| l.len() - l.trim_start().len())
390 .unwrap_or(0);
391
392 let mut end = start + 1;
394 while end < lines.len() {
395 let line = lines[end];
396 let trimmed = line.trim_start();
397
398 if trimmed.is_empty() {
400 end += 1;
401 continue;
402 }
403
404 let current_indent = line.len() - trimmed.len();
405
406 if current_indent <= base_indent && !trimmed.starts_with('-') {
408 break;
409 }
410
411 end += 1;
412 }
413
414 Ok((start, end))
415}
416
417fn detect_indent(lines: &[&str], line_idx: usize) -> String {
419 lines
420 .get(line_idx)
421 .map(|l| {
422 let trimmed = l.trim_start();
423 let indent_len = l.len() - trimmed.len();
424 " ".repeat(indent_len)
425 })
426 .unwrap_or_else(|| " ".to_string()) }
428
429fn generate_resources_yaml(fix: &PreciseFix, indent: &str) -> String {
431 let child_indent = format!("{} ", indent);
432
433 let mut yaml = format!("{}resources:\n", indent);
434 yaml.push_str(&format!("{}requests:\n", child_indent));
435
436 if let Some(ref cpu) = fix.recommended.cpu_request {
437 yaml.push_str(&format!("{} cpu: \"{}\"\n", child_indent, cpu));
438 }
439 if let Some(ref mem) = fix.recommended.memory_request {
440 yaml.push_str(&format!("{} memory: \"{}\"\n", child_indent, mem));
441 }
442
443 yaml.push_str(&format!("{}limits:\n", child_indent));
444
445 if let Some(ref cpu) = fix.recommended.cpu_limit {
446 yaml.push_str(&format!("{} cpu: \"{}\"\n", child_indent, cpu));
447 }
448 if let Some(ref mem) = fix.recommended.memory_limit {
449 yaml.push_str(&format!("{} memory: \"{}\"", child_indent, mem));
450 }
451
452 yaml
453}
454
455fn generate_fix_id(workload: &str, container: &str) -> String {
457 use std::time::{SystemTime, UNIX_EPOCH};
458 let ts = SystemTime::now()
459 .duration_since(UNIX_EPOCH)
460 .map(|d| d.as_millis())
461 .unwrap_or(0);
462 format!("fix-{}-{}-{}", workload, container, ts % 10000)
463}
464
465fn severity_to_confidence(severity: &Severity) -> u8 {
467 match severity {
468 Severity::Critical => 95,
469 Severity::High => 80,
470 Severity::Medium => 60,
471 Severity::Low => 40,
472 Severity::Info => 20,
473 }
474}
475
476fn assess_impact(rec: &ResourceRecommendation) -> FixImpact {
478 let risk = match rec.severity {
479 Severity::Critical | Severity::High => FixRisk::High,
480 Severity::Medium => FixRisk::Medium,
481 _ => FixRisk::Low,
482 };
483
484 FixImpact {
485 risk,
486 monthly_savings: 0.0,
487 oom_risk: false,
488 throttle_risk: false,
489 recommendation: rec.message.clone(),
490 }
491}
492
493fn format_millicores(millicores: u64) -> String {
495 if millicores >= 1000 && millicores.is_multiple_of(1000) {
496 format!("{}", millicores / 1000)
497 } else {
498 format!("{}m", millicores)
499 }
500}
501
502fn double_millicores(value: &str) -> String {
504 if value.ends_with('m') {
505 let m: u64 = value.trim_end_matches('m').parse().unwrap_or(100);
506 format!("{}m", m * 2)
507 } else {
508 let cores: f64 = value.parse().unwrap_or(0.5);
509 format!("{}", cores * 2.0)
510 }
511}
512
513fn format_bytes(bytes: u64) -> String {
515 if bytes >= 1024 * 1024 * 1024 && bytes.is_multiple_of(1024 * 1024 * 1024) {
516 format!("{}Gi", bytes / (1024 * 1024 * 1024))
517 } else if bytes >= 1024 * 1024 {
518 format!("{}Mi", bytes / (1024 * 1024))
519 } else if bytes >= 1024 {
520 format!("{}Ki", bytes / 1024)
521 } else {
522 format!("{}", bytes)
523 }
524}