use crate::utils::MergeOptions;
use anyhow::{Context, Result};
use kube::config::Kubeconfig;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
fn get_current_kubeconfig_path() -> Option<String> {
dirs::home_dir().map(|home| home.join(".kube/config").to_string_lossy().to_string())
}
fn validate_input(files: &[String], include_current: bool) -> Result<(), String> {
if files.len() < 2 && !include_current {
return Err("✕ At least two kubeconfig files must be provided, or use --current to merge with ~/.kube/config".into());
}
for file in files {
if !PathBuf::from(file).exists() {
return Err(format!("✕ File does not exist: {}", file));
}
}
Ok(())
}
fn confirm_overwrite(path: &str) -> Result<bool, String> {
print!(
"⚠ The file '{}' already exists. Do you want to overwrite it? (Y/n): ",
path
);
io::stdout().flush().ok();
let mut confirmation = String::new();
io::stdin()
.read_line(&mut confirmation)
.map_err(|_| "✕ Failed to read input")?;
let confirmation = confirmation.trim().to_lowercase();
Ok(confirmation.is_empty() || confirmation == "y")
}
fn validate_output_path(path: &str) -> Result<(), String> {
let output_path = PathBuf::from(path);
if output_path.exists() && !confirm_overwrite(path)? {
return Err("✕ Operation cancelled by the user.".into());
}
Ok(())
}
async fn load_and_merge_kubeconfigs(paths: &[String]) -> Result<Kubeconfig> {
let mut merged_config = None;
for path in paths {
let config_str = fs::read_to_string(path)
.with_context(|| format!("Failed to read kubeconfig file: {}", path))?;
let config: Kubeconfig = serde_yaml::from_str(&config_str)
.with_context(|| format!("Failed to parse kubeconfig file: {}", path))?;
match merged_config {
None => merged_config = Some(config),
Some(ref mut merged) => {
merged.clusters.extend(config.clusters);
merged.auth_infos.extend(config.auth_infos);
merged.contexts.extend(config.contexts);
if merged.current_context.is_none() {
merged.current_context = config.current_context;
}
}
}
}
merged_config.ok_or_else(|| anyhow::anyhow!("No valid kubeconfig files found"))
}
pub async fn merge_kubeconfigs(options: MergeOptions) -> Result<String, String> {
let mut files = options.files.clone();
if options.include_current {
if let Some(current_path) = get_current_kubeconfig_path() {
files.insert(0, current_path);
} else {
return Err("✕ Failed to determine the home directory.".into());
}
}
validate_input(&files, options.include_current)?;
let merged_config = load_and_merge_kubeconfigs(&files)
.await
.map_err(|e| format!("✕ Failed to merge kubeconfigs: {}", e))?;
let merged_yaml = serde_yaml::to_string(&merged_config)
.map_err(|e| format!("✕ Failed to serialize merged config: {}", e))?;
if options.dry_run {
return Ok(merged_yaml);
}
let merged_file = options.output_path.unwrap_or_else(|| {
dirs::home_dir()
.map(|home| {
home.join(".kube/config.merged")
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| "/tmp/kubeconfig-merged.yaml".to_string())
});
validate_output_path(&merged_file)?;
fs::write(&merged_file, merged_yaml)
.map_err(|e| format!("✕ Failed to write the merged file: {}", e))?;
Ok(merged_file)
}