diff --git a/src/kubeconfig.rs b/src/kubeconfig.rs index a2628c67..7947ebf0 100644 --- a/src/kubeconfig.rs +++ b/src/kubeconfig.rs @@ -258,6 +258,8 @@ where users: vec![], }; + let mut load_errors: Vec = vec![]; + for path in kubeconfigs.into_iter() { let path = path.as_ref(); @@ -282,11 +284,19 @@ where .extend(kubeconfig.users.drain(..).map(|x| Sourced::new(&path, x))); } Err(err) => { - eprintln!("Error loading kubeconfig {}: {}", path.display(), err); + load_errors.push(format!("Error loading kubeconfig {}: {}", path.display(), err)); } } } + // Only print load errors when no contexts were found, to avoid + // noisy warnings about broken files when valid configs exist. + if installed.contexts.is_empty() { + for err in &load_errors { + eprintln!("{}", err); + } + } + Ok(installed) } diff --git a/src/settings.rs b/src/settings.rs index ee7efed6..d1efa8cd 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -57,8 +57,47 @@ pub struct Settings { pub fzf: Fzf, } +/// Check if a path has a kubie settings filename. +fn is_kubie_settings_name(path: &Path) -> bool { + matches!(path.file_name().and_then(|f| f.to_str()), Some("kubie.yaml") | Some("kubie.yml")) +} + +/// Parse KUBECONFIG into individual entries (colon-separated on unix). +fn parse_kubeconfig_env() -> Vec { + match std::env::var("KUBECONFIG") { + Ok(val) if !val.is_empty() => val.split(':').map(PathBuf::from).collect(), + _ => vec![], + } +} + +/// Find a kubie settings file (kubie.yaml or kubie.yml) in a directory. +fn find_settings_in_dir(dir: &Path) -> Option { + ["kubie.yaml", "kubie.yml"].iter().find_map(|name| { + let c = dir.join(name); + if c.is_file() { c.to_str().map(String::from) } else { None } + }) +} + impl Settings { pub fn path() -> String { + for entry in parse_kubeconfig_env() { + if is_kubie_settings_name(&entry) && entry.is_file() { + if let Some(s) = entry.to_str() { + return s.to_string(); + } + } + if let Some(s) = find_settings_in_dir(&entry) { + return s; + } + } + + let xdg_config = std::env::var("XDG_CONFIG_HOME") + .unwrap_or_else(|_| format!("{}/.config", home_dir())); + let xdg_dir = Path::new(&xdg_config).join("kubie"); + if let Some(s) = find_settings_in_dir(&xdg_dir) { + return s; + } + format!("{}/.kube/kubie.yaml", home_dir()) } @@ -74,23 +113,37 @@ impl Settings { Settings::default() }; - // Very important to exclude kubie's own config file ~/.kube/kubie.yaml from the results. + // Very important to exclude kubie's own config file from the results. settings.configs.exclude.push(settings_path_str); Ok(settings) } pub fn get_kube_configs_paths(&self) -> Result> { let mut paths = HashSet::new(); + + for entry in parse_kubeconfig_env() { + if entry.is_file() && !is_kubie_settings_name(&entry) { + paths.insert(entry); + } else if entry.is_dir() { + for pattern in &["*.yml", "*.yaml"] { + for matched in glob(&format!("{}/{pattern}", entry.display()))? { + let path = matched?; + if !is_kubie_settings_name(&path) { + paths.insert(path); + } + } + } + } + } + for inc in &self.configs.include { - let expanded = expanduser(inc); - for entry in glob(&expanded)? { + for entry in glob(&expanduser(inc))? { paths.insert(entry?); } } for exc in &self.configs.exclude { - let expanded = expanduser(exc); - for entry in glob(&expanded)? { + for entry in glob(&expanduser(exc))? { paths.remove(&entry?); } } @@ -224,10 +277,110 @@ fn def_bool_false() -> bool { false } -#[test] -fn test_expanduser() { - assert_eq!( - expanduser("~/hello/world/*.foo"), - format!("{}/hello/world/*.foo", home_dir()) - ); +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_expanduser() { + assert_eq!( + expanduser("~/hello/world/*.foo"), + format!("{}/hello/world/*.foo", home_dir()) + ); + } + + #[test] + fn test_kubeconfig_env_file() { + let dir = tempfile::tempdir().unwrap(); + let file_path = dir.path().join("test-cluster.yaml"); + fs::write(&file_path, "apiVersion: v1").unwrap(); + + std::env::set_var("KUBECONFIG", file_path.to_str().unwrap()); + + let settings = Settings { + configs: Configs { + include: vec![], + exclude: vec![], + }, + ..Settings::default() + }; + + let paths = settings.get_kube_configs_paths().unwrap(); + assert!(paths.contains(&file_path)); + + std::env::remove_var("KUBECONFIG"); + } + + #[test] + fn test_kubeconfig_env_directory() { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("a.yaml"), "apiVersion: v1").unwrap(); + fs::write(dir.path().join("b.yml"), "apiVersion: v1").unwrap(); + fs::write(dir.path().join("c.txt"), "not a kubeconfig").unwrap(); + fs::write(dir.path().join("kubie.yaml"), "configs: {}").unwrap(); + + std::env::set_var("KUBECONFIG", dir.path().to_str().unwrap()); + + let settings = Settings { + configs: Configs { + include: vec![], + exclude: vec![], + }, + ..Settings::default() + }; + + let paths = settings.get_kube_configs_paths().unwrap(); + assert!(paths.contains(&dir.path().join("a.yaml"))); + assert!(paths.contains(&dir.path().join("b.yml"))); + assert!(!paths.contains(&dir.path().join("c.txt"))); + assert!(!paths.contains(&dir.path().join("kubie.yaml"))); + + std::env::remove_var("KUBECONFIG"); + } + + #[test] + fn test_kubeconfig_env_picks_up_kubie_settings() { + let dir = tempfile::tempdir().unwrap(); + fs::write(dir.path().join("kubie.yaml"), "configs: {}").unwrap(); + + std::env::set_var("KUBECONFIG", dir.path().to_str().unwrap()); + + let path = Settings::path(); + assert_eq!(path, dir.path().join("kubie.yaml").to_str().unwrap()); + + std::env::remove_var("KUBECONFIG"); + } + + #[test] + fn test_kubeconfig_env_unset() { + std::env::remove_var("KUBECONFIG"); + + let settings = Settings { + configs: Configs { + include: vec![], + exclude: vec![], + }, + ..Settings::default() + }; + + let paths = settings.get_kube_configs_paths().unwrap(); + assert!(paths.is_empty()); + } + + #[test] + fn test_xdg_config_home() { + std::env::remove_var("KUBECONFIG"); + let dir = tempfile::tempdir().unwrap(); + let kubie_dir = dir.path().join("kubie"); + fs::create_dir_all(&kubie_dir).unwrap(); + fs::write(kubie_dir.join("kubie.yaml"), "configs: {}").unwrap(); + + std::env::set_var("XDG_CONFIG_HOME", dir.path().to_str().unwrap()); + + let path = Settings::path(); + assert_eq!(path, kubie_dir.join("kubie.yaml").to_str().unwrap()); + + std::env::remove_var("XDG_CONFIG_HOME"); + } }