use std::{path::PathBuf, time::Duration};
use thiserror::Error;
mod file_config;
mod file_loader;
mod incluster_config;
use file_loader::ConfigLoader;
pub use file_loader::KubeConfigOptions;
pub use incluster_config::Error as InClusterError;
#[derive(Error, Debug)]
#[error("failed to infer config: in-cluster: ({in_cluster}), kubeconfig: ({kubeconfig})")]
pub struct InferConfigError {
in_cluster: InClusterError,
#[source]
kubeconfig: KubeconfigError,
}
#[derive(Error, Debug)]
pub enum KubeconfigError {
#[error("failed to determine current context")]
CurrentContextNotSet,
#[error("kubeconfigs with mismatching kind cannot be merged")]
KindMismatch,
#[error("kubeconfigs with mismatching api version cannot be merged")]
ApiVersionMismatch,
#[error("failed to load current context: {0}")]
LoadContext(String),
#[error("failed to load the cluster of context: {0}")]
LoadClusterOfContext(String),
#[error("failed to find named user: {0}")]
FindUser(String),
#[error("failed to find the path of kubeconfig")]
FindPath,
#[error("failed to read kubeconfig from '{1:?}': {0}")]
ReadConfig(#[source] std::io::Error, PathBuf),
#[error("failed to parse kubeconfig YAML: {0}")]
Parse(#[source] serde_yaml::Error),
#[error("the structure of the parsed kubeconfig is invalid: {0}")]
InvalidStructure(#[source] serde_yaml::Error),
#[error("failed to parse cluster url: {0}")]
ParseClusterUrl(#[source] http::uri::InvalidUri),
#[error("failed to parse proxy url: {0}")]
ParseProxyUrl(#[source] http::uri::InvalidUri),
#[error("failed to load certificate authority")]
LoadCertificateAuthority(#[source] LoadDataError),
#[error("failed to load client certificate")]
LoadClientCertificate(#[source] LoadDataError),
#[error("failed to load client key")]
LoadClientKey(#[source] LoadDataError),
#[error("failed to parse PEM-encoded certificates: {0}")]
ParseCertificates(#[source] pem::PemError),
}
#[derive(Debug, Error)]
pub enum LoadDataError {
#[error("failed to decode base64 data: {0}")]
DecodeBase64(#[source] base64::DecodeError),
#[error("failed to read file '{1:?}': {0}")]
ReadFile(#[source] std::io::Error, PathBuf),
#[error("no base64 data or file")]
NoBase64DataOrFile,
}
#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
#[derive(Debug, Clone)]
pub struct Config {
pub cluster_url: http::Uri,
pub default_namespace: String,
pub root_cert: Option<Vec<Vec<u8>>>,
pub timeout: Option<std::time::Duration>,
pub accept_invalid_certs: bool,
pub(crate) auth_info: AuthInfo,
pub proxy_url: Option<http::Uri>,
}
impl Config {
pub fn new(cluster_url: http::Uri) -> Self {
Self {
cluster_url,
default_namespace: String::from("default"),
root_cert: None,
timeout: Some(DEFAULT_TIMEOUT),
accept_invalid_certs: false,
auth_info: AuthInfo::default(),
proxy_url: None,
}
}
pub async fn infer() -> Result<Self, InferConfigError> {
let mut config = match Self::from_kubeconfig(&KubeConfigOptions::default()).await {
Err(kubeconfig_err) => {
tracing::trace!(
error = &kubeconfig_err as &dyn std::error::Error,
"no local config found, falling back to local in-cluster config"
);
Self::from_cluster_env().map_err(|in_cluster_err| InferConfigError {
in_cluster: in_cluster_err,
kubeconfig: kubeconfig_err,
})?
}
Ok(success) => success,
};
config.apply_debug_overrides();
Ok(config)
}
pub fn from_cluster_env() -> Result<Self, InClusterError> {
let cluster_url = incluster_config::kube_dns();
let default_namespace = incluster_config::load_default_ns()?;
let root_cert = incluster_config::load_cert()?;
Ok(Self {
cluster_url,
default_namespace,
root_cert: Some(root_cert),
timeout: Some(DEFAULT_TIMEOUT),
accept_invalid_certs: false,
auth_info: AuthInfo {
token_file: Some(incluster_config::token_file()),
..Default::default()
},
proxy_url: None,
})
}
pub async fn from_kubeconfig(options: &KubeConfigOptions) -> Result<Self, KubeconfigError> {
let loader = ConfigLoader::new_from_options(options).await?;
Self::new_from_loader(loader).await
}
pub async fn from_custom_kubeconfig(
kubeconfig: Kubeconfig,
options: &KubeConfigOptions,
) -> Result<Self, KubeconfigError> {
let loader = ConfigLoader::new_from_kubeconfig(kubeconfig, options).await?;
Self::new_from_loader(loader).await
}
async fn new_from_loader(loader: ConfigLoader) -> Result<Self, KubeconfigError> {
let cluster_url = loader
.cluster
.server
.parse::<http::Uri>()
.map_err(KubeconfigError::ParseClusterUrl)?;
let default_namespace = loader
.current_context
.namespace
.clone()
.unwrap_or_else(|| String::from("default"));
let mut accept_invalid_certs = loader.cluster.insecure_skip_tls_verify.unwrap_or(false);
let mut root_cert = None;
if let Some(ca_bundle) = loader.ca_bundle()? {
for ca in &ca_bundle {
accept_invalid_certs = hacky_cert_lifetime_for_macos(ca);
}
root_cert = Some(ca_bundle);
}
Ok(Self {
cluster_url,
default_namespace,
root_cert,
timeout: Some(DEFAULT_TIMEOUT),
accept_invalid_certs,
proxy_url: loader.proxy_url()?,
auth_info: loader.user,
})
}
#[tracing::instrument(level = "warn")]
pub fn apply_debug_overrides(&mut self) {
if let Ok(impersonate_user) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_USER") {
tracing::warn!(?impersonate_user, "impersonating user");
self.auth_info.impersonate = Some(impersonate_user);
}
if let Ok(impersonate_groups) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_GROUP") {
let impersonate_groups = impersonate_groups.split(',').map(str::to_string).collect();
tracing::warn!(?impersonate_groups, "impersonating groups");
self.auth_info.impersonate_groups = Some(impersonate_groups);
}
if let Ok(url) = std::env::var("KUBE_RS_DEBUG_OVERRIDE_URL") {
tracing::warn!(?url, "overriding cluster URL");
match url.parse() {
Ok(uri) => {
self.cluster_url = uri;
}
Err(err) => {
tracing::warn!(
?url,
error = &err as &dyn std::error::Error,
"failed to parse override cluster URL, ignoring"
);
}
}
}
}
pub(crate) fn identity_pem(&self) -> Option<Vec<u8>> {
self.auth_info.identity_pem().ok()
}
}
fn certs(data: &[u8]) -> Result<Vec<Vec<u8>>, pem::PemError> {
Ok(pem::parse_many(data)?
.into_iter()
.filter_map(|p| {
if p.tag == "CERTIFICATE" {
Some(p.contents)
} else {
None
}
})
.collect::<Vec<_>>())
}
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(295);
#[cfg(all(target_os = "macos", feature = "native-tls"))]
fn hacky_cert_lifetime_for_macos(ca: &[u8]) -> bool {
use openssl::x509::X509;
let ca = X509::from_der(ca).expect("valid der is a der");
ca.not_before()
.diff(ca.not_after())
.map(|d| d.days.abs() > 824)
.unwrap_or(false)
}
#[cfg(any(not(target_os = "macos"), not(feature = "native-tls")))]
fn hacky_cert_lifetime_for_macos(_: &[u8]) -> bool {
false
}
pub use file_config::{
AuthInfo, AuthProviderConfig, Cluster, Context, ExecConfig, Kubeconfig, NamedAuthInfo, NamedCluster,
NamedContext, NamedExtension, Preferences,
};
#[cfg(test)]
mod tests {
#[cfg(not(feature = "client"))] #[tokio::test]
async fn config_loading_on_small_feature_set() {
use super::Config;
let cfgraw = r#"
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: aGVsbG8K
server: https://0.0.0.0:6443
name: k3d-test
contexts:
- context:
cluster: k3d-test
user: admin@k3d-test
name: k3d-test
current-context: k3d-test
kind: Config
preferences: {}
users:
- name: admin@k3d-test
user:
client-certificate-data: aGVsbG8K
client-key-data: aGVsbG8K
"#;
let file = tempfile::NamedTempFile::new().expect("create config tempfile");
std::fs::write(file.path(), cfgraw).unwrap();
std::env::set_var("KUBECONFIG", file.path());
let kubeconfig = Config::infer().await.unwrap();
assert_eq!(kubeconfig.cluster_url, "https://0.0.0.0:6443/");
}
}