use std::{path::PathBuf, time::Duration};
use http::{HeaderName, HeaderValue};
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 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("cluster url is missing on selected cluster")]
MissingClusterUrl,
#[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 connect_timeout: Option<std::time::Duration>,
pub read_timeout: Option<std::time::Duration>,
pub write_timeout: Option<std::time::Duration>,
pub accept_invalid_certs: bool,
pub auth_info: AuthInfo,
pub disable_compression: bool,
pub proxy_url: Option<http::Uri>,
pub tls_server_name: Option<String>,
pub headers: Vec<(HeaderName, HeaderValue)>,
}
impl Config {
pub fn new(cluster_url: http::Uri) -> Self {
Self {
cluster_url,
default_namespace: String::from("default"),
root_cert: None,
connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
read_timeout: Some(DEFAULT_READ_TIMEOUT),
write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
accept_invalid_certs: false,
auth_info: AuthInfo::default(),
disable_compression: false,
proxy_url: None,
tls_server_name: None,
headers: Vec::new(),
}
}
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::incluster().map_err(|in_cluster| InferConfigError {
in_cluster,
kubeconfig: kubeconfig_err,
})?
}
Ok(success) => success,
};
config.apply_debug_overrides();
Ok(config)
}
pub fn incluster() -> Result<Self, InClusterError> {
Self::incluster_env()
}
pub fn incluster_env() -> Result<Self, InClusterError> {
let uri = incluster_config::try_kube_from_env()?;
Self::incluster_with_uri(uri)
}
pub fn incluster_dns() -> Result<Self, InClusterError> {
Self::incluster_with_uri(incluster_config::kube_dns())
}
fn incluster_with_uri(cluster_url: http::uri::Uri) -> Result<Self, InClusterError> {
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),
connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
read_timeout: Some(DEFAULT_READ_TIMEOUT),
write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
accept_invalid_certs: false,
auth_info: AuthInfo {
token_file: Some(incluster_config::token_file()),
..Default::default()
},
disable_compression: false,
proxy_url: None,
tls_server_name: None,
headers: Vec::new(),
})
}
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
.clone()
.ok_or(KubeconfigError::MissingClusterUrl)?
.parse::<http::Uri>()
.map_err(KubeconfigError::ParseClusterUrl)?;
let default_namespace = loader
.current_context
.namespace
.clone()
.unwrap_or_else(|| String::from("default"));
let accept_invalid_certs = loader.cluster.insecure_skip_tls_verify.unwrap_or(false);
let disable_compression = loader.cluster.disable_compression.unwrap_or(false);
let mut root_cert = None;
if let Some(ca_bundle) = loader.ca_bundle()? {
root_cert = Some(ca_bundle);
}
Ok(Self {
cluster_url,
default_namespace,
root_cert,
connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
read_timeout: Some(DEFAULT_READ_TIMEOUT),
write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
accept_invalid_certs,
disable_compression,
proxy_url: loader.proxy_url()?,
auth_info: loader.user,
tls_server_name: loader.cluster.tls_server_name,
headers: Vec::new(),
})
}
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.into_contents())
} else {
None
}
})
.collect::<Vec<_>>())
}
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(295);
const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);
pub use file_config::{
AuthInfo, AuthProviderConfig, Cluster, Context, ExecAuthCluster, ExecConfig, ExecInteractiveMode,
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/");
}
}