use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use super::live_analyzer::LiveRecommendation;
use super::types::{
FixApplicationResult, FixImpact, FixResourceValues, FixRisk, FixSource, FixStatus, PreciseFix,
ResourceRecommendation, Severity,
};
#[derive(Debug, Clone)]
pub struct YamlLocation {
pub start_line: u32,
pub resources_line: Option<u32>,
pub resources_column: Option<u32>,
pub yaml_path: String,
}
pub fn locate_resources_in_file(
file_path: &Path,
recommendations: &[LiveRecommendation],
) -> Vec<PreciseFix> {
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(_) => return vec![],
};
let mut fixes = Vec::new();
for doc in yaml_rust2::YamlLoader::load_from_str(&content).unwrap_or_default() {
let locations = find_workload_locations(&content, &doc);
for rec in recommendations {
if let Some(loc) =
locations.get(&(rec.workload_name.clone(), rec.container_name.clone()))
{
let fix = create_precise_fix(file_path, rec, loc);
fixes.push(fix);
}
}
}
fixes
}
pub fn locate_resources_from_static(recommendations: &[ResourceRecommendation]) -> Vec<PreciseFix> {
let mut fixes = Vec::new();
for rec in recommendations {
let fix = PreciseFix {
id: generate_fix_id(&rec.resource_name, &rec.container),
file_path: rec.file_path.clone(),
line_number: rec.line.unwrap_or(0),
column: None,
resource_kind: rec.resource_kind.clone(),
resource_name: rec.resource_name.clone(),
container_name: rec.container.clone(),
namespace: rec.namespace.clone(),
current: FixResourceValues {
cpu_request: rec.current.cpu_request.clone(),
cpu_limit: rec.current.cpu_limit.clone(),
memory_request: rec.current.memory_request.clone(),
memory_limit: rec.current.memory_limit.clone(),
},
recommended: FixResourceValues {
cpu_request: rec.recommended.cpu_request.clone(),
cpu_limit: rec.recommended.cpu_limit.clone(),
memory_request: rec.recommended.memory_request.clone(),
memory_limit: rec.recommended.memory_limit.clone(),
},
confidence: severity_to_confidence(&rec.severity),
source: FixSource::StaticAnalysis,
impact: assess_impact(rec),
status: FixStatus::Pending,
};
fixes.push(fix);
}
fixes
}
fn find_workload_locations(
content: &str,
_doc: &yaml_rust2::Yaml,
) -> HashMap<(String, String), YamlLocation> {
let mut locations = HashMap::new();
let lines: Vec<&str> = content.lines().collect();
let mut current_kind = String::new();
let mut current_name = String::new();
let mut current_container = String::new();
let mut workload_start_line: u32 = 0;
let mut in_containers = false;
let mut resources_line: Option<u32> = None;
for (idx, line) in lines.iter().enumerate() {
let line_num = (idx + 1) as u32;
let trimmed = line.trim();
if trimmed.starts_with("kind:") {
current_kind = trimmed.trim_start_matches("kind:").trim().to_string();
workload_start_line = line_num;
current_name.clear();
current_container.clear();
in_containers = false;
resources_line = None;
}
if trimmed.starts_with("name:") && !in_containers {
current_name = trimmed.trim_start_matches("name:").trim().to_string();
}
if trimmed == "containers:" {
in_containers = true;
}
if in_containers && trimmed.starts_with("- name:") {
current_container = trimmed.trim_start_matches("- name:").trim().to_string();
}
if in_containers && trimmed == "resources:" {
resources_line = Some(line_num);
if !current_name.is_empty() && !current_container.is_empty() {
let key = (current_name.clone(), current_container.clone());
locations.insert(
key,
YamlLocation {
start_line: workload_start_line,
resources_line,
resources_column: Some(line.len() as u32 - trimmed.len() as u32),
yaml_path: format!(
"{}/{}/containers/{}/resources",
current_kind, current_name, current_container
),
},
);
}
}
}
locations
}
fn create_precise_fix(
file_path: &Path,
rec: &LiveRecommendation,
loc: &YamlLocation,
) -> PreciseFix {
let cpu_str = format_millicores(rec.recommended_cpu_millicores);
let mem_str = format_bytes(rec.recommended_memory_bytes);
let current_cpu = rec.current_cpu_millicores.map(format_millicores);
let current_mem = rec.current_memory_bytes.map(format_bytes);
PreciseFix {
id: generate_fix_id(&rec.workload_name, &rec.container_name),
file_path: file_path.to_path_buf(),
line_number: loc.resources_line.unwrap_or(loc.start_line),
column: loc.resources_column,
resource_kind: rec.workload_kind.clone(),
resource_name: rec.workload_name.clone(),
container_name: rec.container_name.clone(),
namespace: Some(rec.namespace.clone()),
current: FixResourceValues {
cpu_request: current_cpu.clone(),
cpu_limit: current_cpu.map(|c| double_millicores(&c)),
memory_request: current_mem.clone(),
memory_limit: current_mem.clone(),
},
recommended: FixResourceValues {
cpu_request: Some(cpu_str.clone()),
cpu_limit: Some(double_millicores(&cpu_str)),
memory_request: Some(mem_str.clone()),
memory_limit: Some(mem_str),
},
confidence: rec.confidence,
source: match rec.data_source {
super::live_analyzer::DataSource::Prometheus => FixSource::PrometheusP95,
super::live_analyzer::DataSource::MetricsServer => FixSource::MetricsServer,
super::live_analyzer::DataSource::Combined => FixSource::Combined,
super::live_analyzer::DataSource::Static => FixSource::StaticAnalysis,
},
impact: FixImpact {
risk: if rec.confidence >= 80 {
FixRisk::Low
} else if rec.confidence >= 60 {
FixRisk::Medium
} else {
FixRisk::High
},
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 {
"Safe to apply - high confidence based on observed usage".to_string()
} else if rec.confidence >= 60 {
"Review before applying - moderate confidence".to_string()
} else {
"Manual review required - limited data available".to_string()
},
},
status: FixStatus::Pending,
}
}
pub fn apply_fixes(
fixes: &mut [PreciseFix],
backup_dir: Option<&Path>,
dry_run: bool,
min_confidence: u8,
) -> FixApplicationResult {
let mut applied = 0;
let mut skipped = 0;
let mut failed = 0;
let mut errors = Vec::new();
let backup_path = if !dry_run {
if let Some(dir) = backup_dir {
match fs::create_dir_all(dir) {
Ok(_) => Some(dir.to_path_buf()),
Err(e) => {
errors.push(format!("Failed to create backup dir: {}", e));
None
}
}
} else {
None
}
} else {
None
};
let mut fixes_by_file: HashMap<PathBuf, Vec<&mut PreciseFix>> = HashMap::new();
for fix in fixes.iter_mut() {
fixes_by_file
.entry(fix.file_path.clone())
.or_default()
.push(fix);
}
for (file_path, file_fixes) in fixes_by_file.iter_mut() {
let content = match fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
errors.push(format!("Failed to read {}: {}", file_path.display(), e));
for fix in file_fixes.iter_mut() {
fix.status = FixStatus::Failed;
failed += 1;
}
continue;
}
};
if !dry_run && let Some(ref backup) = backup_path {
let backup_file = backup.join(file_path.file_name().unwrap_or_default());
if let Err(e) = fs::write(&backup_file, &content) {
errors.push(format!("Failed to backup {}: {}", file_path.display(), e));
}
}
let mut modified_content = content.clone();
let mut line_offset: i32 = 0;
file_fixes.sort_by(|a, b| b.line_number.cmp(&a.line_number));
for fix in file_fixes.iter_mut() {
if fix.confidence < min_confidence {
fix.status = FixStatus::Skipped;
skipped += 1;
continue;
}
if fix.impact.risk == FixRisk::Critical {
fix.status = FixStatus::Skipped;
skipped += 1;
continue;
}
match apply_single_fix(&mut modified_content, fix, &mut line_offset) {
Ok(_) => {
fix.status = if dry_run {
FixStatus::Pending
} else {
FixStatus::Applied
};
applied += 1;
}
Err(e) => {
fix.status = FixStatus::Failed;
errors.push(format!("Fix {} failed: {}", fix.id, e));
failed += 1;
}
}
}
if !dry_run
&& applied > 0
&& let Err(e) = fs::write(file_path, &modified_content)
{
errors.push(format!("Failed to write {}: {}", file_path.display(), e));
}
}
FixApplicationResult {
total_fixes: fixes.len(),
applied,
skipped,
failed,
backup_path,
fixes: fixes.to_vec(),
errors,
}
}
fn apply_single_fix(
content: &mut String,
fix: &PreciseFix,
_line_offset: &mut i32,
) -> Result<(), String> {
let lines: Vec<&str> = content.lines().collect();
let target_line = fix.line_number as usize;
if target_line == 0 || target_line > lines.len() {
return Err(format!("Invalid line number: {}", target_line));
}
let indent = detect_indent(&lines, target_line - 1);
let new_resources = generate_resources_yaml(fix, &indent);
let (start_idx, end_idx) = find_resources_section(&lines, target_line - 1)?;
let mut new_lines: Vec<String> = Vec::new();
new_lines.extend(lines[..start_idx].iter().map(|s| s.to_string()));
new_lines.push(new_resources);
new_lines.extend(lines[end_idx..].iter().map(|s| s.to_string()));
*content = new_lines.join("\n");
Ok(())
}
fn find_resources_section(lines: &[&str], start: usize) -> Result<(usize, usize), String> {
let base_indent = lines
.get(start)
.map(|l| l.len() - l.trim_start().len())
.unwrap_or(0);
let mut end = start + 1;
while end < lines.len() {
let line = lines[end];
let trimmed = line.trim_start();
if trimmed.is_empty() {
end += 1;
continue;
}
let current_indent = line.len() - trimmed.len();
if current_indent <= base_indent && !trimmed.starts_with('-') {
break;
}
end += 1;
}
Ok((start, end))
}
fn detect_indent(lines: &[&str], line_idx: usize) -> String {
lines
.get(line_idx)
.map(|l| {
let trimmed = l.trim_start();
let indent_len = l.len() - trimmed.len();
" ".repeat(indent_len)
})
.unwrap_or_else(|| " ".to_string()) }
fn generate_resources_yaml(fix: &PreciseFix, indent: &str) -> String {
let child_indent = format!("{} ", indent);
let mut yaml = format!("{}resources:\n", indent);
yaml.push_str(&format!("{}requests:\n", child_indent));
if let Some(ref cpu) = fix.recommended.cpu_request {
yaml.push_str(&format!("{} cpu: \"{}\"\n", child_indent, cpu));
}
if let Some(ref mem) = fix.recommended.memory_request {
yaml.push_str(&format!("{} memory: \"{}\"\n", child_indent, mem));
}
yaml.push_str(&format!("{}limits:\n", child_indent));
if let Some(ref cpu) = fix.recommended.cpu_limit {
yaml.push_str(&format!("{} cpu: \"{}\"\n", child_indent, cpu));
}
if let Some(ref mem) = fix.recommended.memory_limit {
yaml.push_str(&format!("{} memory: \"{}\"", child_indent, mem));
}
yaml
}
fn generate_fix_id(workload: &str, container: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
format!("fix-{}-{}-{}", workload, container, ts % 10000)
}
fn severity_to_confidence(severity: &Severity) -> u8 {
match severity {
Severity::Critical => 95,
Severity::High => 80,
Severity::Medium => 60,
Severity::Low => 40,
Severity::Info => 20,
}
}
fn assess_impact(rec: &ResourceRecommendation) -> FixImpact {
let risk = match rec.severity {
Severity::Critical | Severity::High => FixRisk::High,
Severity::Medium => FixRisk::Medium,
_ => FixRisk::Low,
};
FixImpact {
risk,
monthly_savings: 0.0,
oom_risk: false,
throttle_risk: false,
recommendation: rec.message.clone(),
}
}
fn format_millicores(millicores: u64) -> String {
if millicores >= 1000 && millicores.is_multiple_of(1000) {
format!("{}", millicores / 1000)
} else {
format!("{}m", millicores)
}
}
fn double_millicores(value: &str) -> String {
if value.ends_with('m') {
let m: u64 = value.trim_end_matches('m').parse().unwrap_or(100);
format!("{}m", m * 2)
} else {
let cores: f64 = value.parse().unwrap_or(0.5);
format!("{}", cores * 2.0)
}
}
fn format_bytes(bytes: u64) -> String {
if bytes >= 1024 * 1024 * 1024 && bytes.is_multiple_of(1024 * 1024 * 1024) {
format!("{}Gi", bytes / (1024 * 1024 * 1024))
} else if bytes >= 1024 * 1024 {
format!("{}Mi", bytes / (1024 * 1024))
} else if bytes >= 1024 {
format!("{}Ki", bytes / 1024)
} else {
format!("{}", bytes)
}
}