diff --git a/kube/src/config/file_config.rs b/kube/src/config/file_config.rs index ca2c0dec5..f26985103 100644 --- a/kube/src/config/file_config.rs +++ b/kube/src/config/file_config.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// This type (and its children) are exposed for convenience only. /// Please load a [`Config`][crate::Config] object for use with a [`Client`][crate::Client] /// which will read and parse the kubeconfig file -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct Kubeconfig { pub kind: Option, #[serde(rename = "apiVersion")] @@ -23,7 +23,7 @@ pub struct Kubeconfig { pub auth_infos: Vec, pub contexts: Vec, #[serde(rename = "current-context")] - pub current_context: String, + pub current_context: Option, pub extensions: Option>, } @@ -132,6 +132,8 @@ pub struct Context { pub extensions: Option>, } +const KUBECONFIG: &str = "KUBECONFIG"; + /// Some helpers on the raw Config object are exposed for people needing to parse it impl Kubeconfig { /// Read a Config from an arbitrary location @@ -140,14 +142,126 @@ impl Kubeconfig { path: path.as_ref().into(), source, })?; - let config = serde_yaml::from_reader(f).map_err(ConfigError::ParseYaml)?; + let mut config: Kubeconfig = serde_yaml::from_reader(f).map_err(ConfigError::ParseYaml)?; + + // Remap all files we read to absolute paths. + if let Some(dir) = path.as_ref().parent() { + for named in config.clusters.iter_mut() { + if let Some(path) = &named.cluster.certificate_authority { + if let Some(abs_path) = to_absolute(dir, path) { + named.cluster.certificate_authority = Some(abs_path); + } + } + } + for named in config.auth_infos.iter_mut() { + if let Some(path) = &named.auth_info.client_certificate { + if let Some(abs_path) = to_absolute(dir, path) { + named.auth_info.client_certificate = Some(abs_path); + } + } + if let Some(path) = &named.auth_info.client_key { + if let Some(abs_path) = to_absolute(dir, path) { + named.auth_info.client_key = Some(abs_path); + } + } + if let Some(path) = &named.auth_info.token_file { + if let Some(abs_path) = to_absolute(dir, path) { + named.auth_info.token_file = Some(abs_path); + } + } + } + } Ok(config) } - /// Read a Config from the default location + /// Read a Config from `KUBECONFIG` or the the default location. pub fn read() -> Result { - let path = utils::find_kubeconfig()?; - Self::read_from(path) + match Self::from_env()? { + Some(config) => Ok(config), + None => { + let path = utils::default_kube_path().ok_or(ConfigError::NoKubeconfigPath)?; + Self::read_from(path) + } + } + } + + /// Create `Kubeconfig` from `KUBECONFIG` environment variable. + /// Supports list of files to be merged. + /// + /// # Panics + /// + /// Panics if `KUBECONFIG` value contains the NUL character. + pub fn from_env() -> Result> { + match std::env::var_os(KUBECONFIG) { + Some(value) => { + let paths = std::env::split_paths(&value) + .filter(|p| !p.as_os_str().is_empty()) + .collect::>(); + if paths.is_empty() { + return Ok(None); + } + + let merged = paths.iter().try_fold(Kubeconfig::default(), |m, p| { + Kubeconfig::read_from(p).and_then(|c| m.merge(c)) + })?; + Ok(Some(merged)) + } + + None => Ok(None), + } + } + + /// Merge kubeconfig file according to the rules described in + /// + /// + /// > Merge the files listed in the `KUBECONFIG` environment variable according to these rules: + /// > + /// > - Ignore empty filenames. + /// > - Produce errors for files with content that cannot be deserialized. + /// > - The first file to set a particular value or map key wins. + /// > - Never change the value or map key. + /// > Example: Preserve the context of the first file to set `current-context`. + /// > Example: If two files specify a `red-user`, use only values from the first file's `red-user`. + /// > Even if the second file has non-conflicting entries under `red-user`, discard them. + fn merge(mut self, next: Kubeconfig) -> Result { + if self.kind.is_some() && next.kind.is_some() && self.kind != next.kind { + return Err(ConfigError::KindMismatch.into()); + } + if self.api_version.is_some() && next.api_version.is_some() && self.api_version != next.api_version { + return Err(ConfigError::ApiVersionMismatch.into()); + } + + self.kind = self.kind.or(next.kind); + self.api_version = self.api_version.or(next.api_version); + self.preferences = self.preferences.or(next.preferences); + append_new_named(&mut self.clusters, next.clusters, |x| &x.name); + append_new_named(&mut self.auth_infos, next.auth_infos, |x| &x.name); + append_new_named(&mut self.contexts, next.contexts, |x| &x.name); + self.current_context = self.current_context.or(next.current_context); + self.extensions = self.extensions.or(next.extensions); + Ok(self) + } +} + +fn append_new_named(base: &mut Vec, next: Vec, f: F) +where + F: Fn(&T) -> &String, +{ + use std::collections::HashSet; + base.extend({ + let existing = base.iter().map(|x| f(x)).collect::>(); + next.into_iter() + .filter(|x| !existing.contains(f(x))) + .collect::>() + }); +} + +fn to_absolute(dir: &Path, file: &str) -> Option { + let path = Path::new(&file); + if path.is_relative() { + dir.join(path).to_str().map(str::to_owned) + } else { + None } } @@ -171,3 +285,55 @@ impl AuthInfo { utils::data_or_file_with_base64(&self.client_key_data, &self.client_key) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kubeconfig_merge() { + let kubeconfig1 = Kubeconfig { + current_context: Some("default".into()), + auth_infos: vec![NamedAuthInfo { + name: "red-user".into(), + auth_info: AuthInfo { + token: Some("first-token".into()), + ..Default::default() + }, + }], + ..Default::default() + }; + let kubeconfig2 = Kubeconfig { + current_context: Some("dev".into()), + auth_infos: vec![ + NamedAuthInfo { + name: "red-user".into(), + auth_info: AuthInfo { + token: Some("second-token".into()), + username: Some("red-user".into()), + ..Default::default() + }, + }, + NamedAuthInfo { + name: "green-user".into(), + auth_info: AuthInfo { + token: Some("new-token".into()), + ..Default::default() + }, + }, + ], + ..Default::default() + }; + + let merged = kubeconfig1.merge(kubeconfig2).unwrap(); + // Preserves first `current_context` + assert_eq!(merged.current_context, Some("default".into())); + // Auth info with the same name does not overwrite + assert_eq!(merged.auth_infos[0].name, "red-user".to_owned()); + assert_eq!(merged.auth_infos[0].auth_info.token, Some("first-token".into())); + // Even if it's not conflicting + assert_eq!(merged.auth_infos[0].auth_info.username, None); + // New named auth info is appended + assert_eq!(merged.auth_infos[1].name, "green-user".to_owned()); + } +} diff --git a/kube/src/config/file_loader.rs b/kube/src/config/file_loader.rs index f34be422d..fa6fa315f 100644 --- a/kube/src/config/file_loader.rs +++ b/kube/src/config/file_loader.rs @@ -27,11 +27,7 @@ pub struct ConfigLoader { impl ConfigLoader { /// Returns a config loader based on the cluster information from the kubeconfig file. pub async fn new_from_options(options: &KubeConfigOptions) -> Result { - let kubeconfig_path = utils::find_kubeconfig() - .map_err(Box::new) - .map_err(ConfigError::LoadConfigFile)?; - - let config = Kubeconfig::read_from(kubeconfig_path)?; + let config = Kubeconfig::read()?; let loader = Self::load( config, options.context.as_ref(), @@ -61,7 +57,13 @@ impl ConfigLoader { cluster: Option<&String>, user: Option<&String>, ) -> Result { - let context_name = context.unwrap_or(&config.current_context); + let context_name = if let Some(name) = context { + name + } else if let Some(name) = &config.current_context { + name + } else { + return Err(ConfigError::CurrentContextNotSet.into()); + }; let current_context = config .contexts .iter() diff --git a/kube/src/config/incluster_config.rs b/kube/src/config/incluster_config.rs index 925f6ee35..760706e08 100644 --- a/kube/src/config/incluster_config.rs +++ b/kube/src/config/incluster_config.rs @@ -25,18 +25,17 @@ fn kube_port() -> Option { /// Returns token from specified path in cluster. pub fn load_token() -> Result { - utils::data_or_file(&None, &Some(SERVICE_TOKENFILE)) + utils::read_file_to_string(SERVICE_TOKENFILE) } /// Returns certification from specified path in cluster. pub fn load_cert() -> Result>> { - let ca = utils::data_or_file_with_base64(&None, &Some(SERVICE_CERTFILE))?; - Ok(utils::certs(&ca)) + Ok(utils::certs(&utils::read_file(SERVICE_CERTFILE)?)) } /// Returns the default namespace from specified path in cluster. pub fn load_default_ns() -> Result { - utils::data_or_file(&None, &Some(SERVICE_DEFAULT_NS)) + utils::read_file_to_string(SERVICE_DEFAULT_NS) } #[test] diff --git a/kube/src/config/mod.rs b/kube/src/config/mod.rs index 9f46bd541..59885f745 100644 --- a/kube/src/config/mod.rs +++ b/kube/src/config/mod.rs @@ -12,7 +12,7 @@ mod utils; use crate::{error::ConfigError, Result}; use file_loader::ConfigLoader; pub use file_loader::KubeConfigOptions; -pub(crate) use utils::data_or_file; +pub(crate) use utils::read_file_to_string; use http::header::HeaderMap; diff --git a/kube/src/config/utils.rs b/kube/src/config/utils.rs index ac39403f0..62bbde174 100644 --- a/kube/src/config/utils.rs +++ b/kube/src/config/utils.rs @@ -1,25 +1,11 @@ use std::{ - env, fs, + fs, path::{Path, PathBuf}, }; use crate::{error::ConfigError, Error, Result}; use dirs::home_dir; -const KUBECONFIG: &str = "KUBECONFIG"; - -/// Search the kubeconfig file -pub fn find_kubeconfig() -> Result { - kubeconfig_path() - .or_else(default_kube_path) - .ok_or_else(|| ConfigError::NoKubeconfigPath.into()) -} - -/// Returns kubeconfig path from specified environment variable. -pub fn kubeconfig_path() -> Option { - env::var_os(KUBECONFIG).map(PathBuf::from) -} - /// Returns kubeconfig path from `$HOME/.kube/config`. pub fn default_kube_path() -> Option { home_dir().map(|h| h.join(".kube").join("config")) @@ -30,42 +16,29 @@ pub fn data_or_file_with_base64>(data: &Option, file: &Op (Some(d), _) => base64::decode(&d) .map_err(ConfigError::Base64Decode) .map_err(Error::Kubeconfig), - (_, Some(f)) => { - let f = (*f).as_ref(); - let abs_file = if f.is_absolute() { - f.to_path_buf() - } else { - find_kubeconfig().and_then(|cfg| { - cfg.parent() - .map(|kubedir| kubedir.join(f)) - .ok_or_else(|| ConfigError::NoAbsolutePath { path: f.into() }.into()) - })? - }; - // dbg!(&abs_file); - fs::read(&abs_file).map_err(|source| { - ConfigError::ReadFile { - path: abs_file, - source, - } - .into() - }) - } + (_, Some(f)) => read_file(f), _ => Err(ConfigError::NoBase64FileOrData.into()), } } -pub fn data_or_file>(data: &Option, file: &Option

) -> Result { - match (data, file) { - (Some(d), _) => Ok(d.to_string()), - (_, Some(f)) => fs::read_to_string(f).map_err(|source| { - ConfigError::ReadFile { - path: f.as_ref().into(), - source, - } - .into() - }), - _ => Err(ConfigError::NoFileOrData.into()), - } +pub fn read_file>(file: P) -> Result> { + fs::read(&file).map_err(|source| { + ConfigError::ReadFile { + path: file.as_ref().into(), + source, + } + .into() + }) +} + +pub fn read_file_to_string>(file: P) -> Result { + fs::read_to_string(&file).map_err(|source| { + ConfigError::ReadFile { + path: file.as_ref().into(), + source, + } + .into() + }) } pub fn certs(data: &[u8]) -> Vec> { @@ -80,35 +53,3 @@ pub fn certs(data: &[u8]) -> Vec> { }) .collect::>() } - -#[cfg(test)] -mod tests { - extern crate tempfile; - use super::*; - use crate::config::utils; - use std::io::Write; - - #[test] - fn test_kubeconfig_path() { - let expect_str = "/fake/.kube/config"; - env::set_var(KUBECONFIG, expect_str); - assert_eq!(PathBuf::from(expect_str), kubeconfig_path().unwrap()); - } - - #[test] - fn test_data_or_file() { - let data = "fake_data"; - let file = "fake_file"; - let mut tmpfile = tempfile::NamedTempFile::new().unwrap(); - write!(tmpfile, "{}", file).unwrap(); - - let actual = utils::data_or_file(&Some(data.to_string()), &Some(tmpfile.path())); - assert_eq!(actual.ok().unwrap(), data.to_string()); - - let actual = utils::data_or_file(&None, &Some(tmpfile.path())); - assert_eq!(actual.ok().unwrap(), file.to_string()); - - let actual = utils::data_or_file(&None, &None::); - assert!(actual.is_err()); - } -} diff --git a/kube/src/error.rs b/kube/src/error.rs index 4bd5f18ed..1a4d83632 100644 --- a/kube/src/error.rs +++ b/kube/src/error.rs @@ -120,6 +120,14 @@ pub enum ConfigError { kubeconfig: Box, }, + #[error("Failed to determine current context")] + CurrentContextNotSet, + + #[error("Merging kubeconfig with mismatching kind")] + KindMismatch, + #[error("Merging kubeconfig with mismatching apiVersion")] + ApiVersionMismatch, + #[error("Unable to load in cluster config, {hostenv} and {portenv} must be defined")] /// One or more required in-cluster config options are missing MissingInClusterVariables { diff --git a/kube/src/service/auth/mod.rs b/kube/src/service/auth/mod.rs index 82b1e5e7e..f0fb60568 100644 --- a/kube/src/service/auth/mod.rs +++ b/kube/src/service/auth/mod.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use crate::{ - config::{data_or_file, AuthInfo, AuthProviderConfig, ExecConfig}, + config::{read_file_to_string, AuthInfo, AuthProviderConfig, ExecConfig}, error::{ConfigError, Error}, Result, }; @@ -119,6 +119,10 @@ impl TryFrom<&AuthInfo> for Authentication { } } + if let (Some(u), Some(p)) = (&auth_info.username, &auth_info.password) { + return Ok(Authentication::Basic(base64::encode(&format!("{}:{}", u, p)))); + } + let (raw_token, expiration) = match &auth_info.token { Some(token) => (Some(token.clone()), None), None => { @@ -133,22 +137,19 @@ impl TryFrom<&AuthInfo> for Authentication { None => None, }; (status.token, expiration) + } else if let Some(file) = &auth_info.token_file { + (Some(read_file_to_string(file)?), None) } else { (None, None) } } }; - match ( - data_or_file(&raw_token, &auth_info.token_file), - (&auth_info.username, &auth_info.password), - expiration, - ) { - (Ok(token), _, None) => Ok(Authentication::Token(token)), - (Ok(token), _, Some(expire)) => Ok(Authentication::RefreshableToken(RefreshableToken::Exec( + match (raw_token, expiration) { + (Some(token), None) => Ok(Authentication::Token(token)), + (Some(token), Some(expire)) => Ok(Authentication::RefreshableToken(RefreshableToken::Exec( Arc::new(Mutex::new((token, expire, auth_info.clone()))), ))), - (_, (Some(u), Some(p)), _) => Ok(Authentication::Basic(base64::encode(&format!("{}:{}", u, p)))), _ => Ok(Authentication::None), } }