1use std::{path::PathBuf, time::Duration};
11
12use http::{HeaderName, HeaderValue};
13use thiserror::Error;
14
15mod file_config;
16mod file_loader;
17mod incluster_config;
18#[cfg(any(feature = "http-proxy", feature = "socks5"))]
19mod no_proxy;
20
21use file_loader::ConfigLoader;
22pub use file_loader::KubeConfigOptions;
23pub use incluster_config::Error as InClusterError;
24#[cfg(any(feature = "http-proxy", feature = "socks5"))]
25pub use no_proxy::Error as NoProxyError;
26
27#[derive(Error, Debug)]
29#[error("failed to infer config: in-cluster: ({in_cluster}), kubeconfig: ({kubeconfig})")]
30pub struct InferConfigError {
31 in_cluster: Box<InClusterError>,
32 #[source]
34 kubeconfig: KubeconfigError,
35}
36
37#[derive(Error, Debug)]
39pub enum KubeconfigError {
40 #[error("failed to determine current context")]
42 CurrentContextNotSet,
43
44 #[error("kubeconfigs with mismatching kind cannot be merged")]
46 KindMismatch,
47
48 #[error("kubeconfigs with mismatching api version cannot be merged")]
50 ApiVersionMismatch,
51
52 #[error("failed to load current context: {0}")]
54 LoadContext(String),
55
56 #[error("failed to load the cluster of context: {0}")]
58 LoadClusterOfContext(String),
59
60 #[error("failed to find the path of kubeconfig")]
62 FindPath,
63
64 #[error("failed to read kubeconfig from '{1:?}': {0}")]
66 ReadConfig(#[source] std::io::Error, PathBuf),
67
68 #[error("failed to parse kubeconfig YAML: {0}")]
70 Parse(Box<serde_saphyr::Error>),
71
72 #[error("cluster url is missing on selected cluster")]
74 MissingClusterUrl,
75
76 #[error("failed to parse cluster url: {0}")]
78 ParseClusterUrl(#[source] http::uri::InvalidUri),
79
80 #[error("failed to parse proxy url: {0}")]
82 ParseProxyUrl(#[source] http::uri::InvalidUri),
83
84 #[error("failed to load certificate authority: {0}")]
86 LoadCertificateAuthority(#[source] LoadDataError),
87
88 #[error("failed to load client certificate: {0}")]
90 LoadClientCertificate(#[source] LoadDataError),
91
92 #[error("failed to load client key: {0}")]
94 LoadClientKey(#[source] LoadDataError),
95
96 #[error("failed to parse PEM-encoded certificates: {0}")]
98 ParseCertificates(#[source] pem::PemError),
99
100 #[cfg(any(feature = "http-proxy", feature = "socks5"))]
102 #[error("failed to parse NO_PROXY/no_proxy env var")]
103 ParseNoProxy(#[source] NoProxyError),
104}
105
106#[derive(Debug, Error)]
108pub enum LoadDataError {
109 #[error("failed to decode base64 data: {0}")]
111 DecodeBase64(#[source] base64::DecodeError),
112
113 #[error("failed to read file '{1:?}': {0}")]
115 ReadFile(#[source] std::io::Error, PathBuf),
116
117 #[error("no base64 data or file")]
119 NoBase64DataOrFile,
120}
121
122#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
135#[derive(Debug, Clone)]
136#[non_exhaustive]
137pub struct Config {
138 pub cluster_url: http::Uri,
140 pub default_namespace: String,
142 pub root_cert: Option<Vec<Vec<u8>>>,
144 pub root_cert_file: Option<PathBuf>,
153 pub connect_timeout: Option<std::time::Duration>,
157 pub read_timeout: Option<std::time::Duration>,
170 pub write_timeout: Option<std::time::Duration>,
174 pub reset_reader_on_write: bool,
184 pub accept_invalid_certs: bool,
186 pub auth_info: AuthInfo,
188 pub disable_compression: bool,
190 pub proxy_url: Option<http::Uri>,
192 pub tls_server_name: Option<String>,
196 pub headers: Vec<(HeaderName, HeaderValue)>,
198 pub default_retry: bool,
200}
201
202impl Config {
203 pub fn new(cluster_url: http::Uri) -> Self {
209 Self {
210 cluster_url,
211 default_namespace: String::from("default"),
212 root_cert: None,
213 root_cert_file: None,
214 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
215 read_timeout: None,
216 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
217 reset_reader_on_write: false,
218 accept_invalid_certs: false,
219 auth_info: AuthInfo::default(),
220 disable_compression: false,
221 proxy_url: None,
222 tls_server_name: None,
223 headers: Vec::new(),
224 default_retry: true,
225 }
226 }
227
228 pub async fn infer() -> Result<Self, InferConfigError> {
238 let mut config = match Self::from_kubeconfig(&KubeConfigOptions::default()).await {
239 Err(kubeconfig_err) => {
240 tracing::trace!(
241 error = &kubeconfig_err as &dyn std::error::Error,
242 "no local config found, falling back to local in-cluster config"
243 );
244
245 Self::incluster().map_err(|in_cluster| InferConfigError {
246 in_cluster: Box::new(in_cluster),
247 kubeconfig: kubeconfig_err,
248 })?
249 }
250 Ok(success) => success,
251 };
252 config.apply_debug_overrides();
253 Ok(config)
254 }
255
256 pub fn incluster() -> Result<Self, InClusterError> {
259 Self::incluster_env()
260 }
261
262 pub fn incluster_env() -> Result<Self, InClusterError> {
271 let uri = incluster_config::try_kube_from_env()?;
272 Self::incluster_with_uri(uri)
273 }
274
275 pub fn incluster_dns() -> Result<Self, InClusterError> {
285 Self::incluster_with_uri(incluster_config::kube_dns())
286 }
287
288 fn incluster_with_uri(cluster_url: http::uri::Uri) -> Result<Self, InClusterError> {
289 let default_namespace = incluster_config::load_default_ns()?;
290 let root_cert = incluster_config::load_cert()?;
291
292 Ok(Self {
293 cluster_url,
294 default_namespace,
295 root_cert: Some(root_cert),
296 root_cert_file: Some(PathBuf::from(incluster_config::cert_file())),
297 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
298 read_timeout: None,
299 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
300 reset_reader_on_write: false,
301 accept_invalid_certs: false,
302 auth_info: AuthInfo {
303 token_file: Some(incluster_config::token_file()),
304 ..Default::default()
305 },
306 disable_compression: false,
307 proxy_url: None,
308 tls_server_name: None,
309 headers: Vec::new(),
310 default_retry: true,
311 })
312 }
313
314 pub async fn from_kubeconfig(options: &KubeConfigOptions) -> Result<Self, KubeconfigError> {
320 let loader = ConfigLoader::new_from_options(options).await?;
321 Self::new_from_loader(loader)
322 }
323
324 pub async fn from_custom_kubeconfig(
328 kubeconfig: Kubeconfig,
329 options: &KubeConfigOptions,
330 ) -> Result<Self, KubeconfigError> {
331 let loader = ConfigLoader::new_from_kubeconfig(kubeconfig, options).await?;
332 Self::new_from_loader(loader)
333 }
334
335 fn new_from_loader(loader: ConfigLoader) -> Result<Self, KubeconfigError> {
336 let cluster_url = loader
337 .cluster
338 .server
339 .clone()
340 .ok_or(KubeconfigError::MissingClusterUrl)?
341 .parse::<http::Uri>()
342 .map_err(KubeconfigError::ParseClusterUrl)?;
343
344 let default_namespace = loader
345 .current_context
346 .namespace
347 .clone()
348 .unwrap_or_else(|| String::from("default"));
349
350 let accept_invalid_certs = loader.cluster.insecure_skip_tls_verify.unwrap_or(false);
351 let disable_compression = loader.cluster.disable_compression.unwrap_or(false);
352
353 let mut root_cert = None;
354
355 if let Some(ca_bundle) = loader.ca_bundle()? {
356 root_cert = Some(ca_bundle);
357 }
358
359 Ok(Self {
360 cluster_url,
361 default_namespace,
362 root_cert,
363 root_cert_file: None,
364 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
365 read_timeout: None,
366 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
367 reset_reader_on_write: false,
368 accept_invalid_certs,
369 disable_compression,
370 proxy_url: loader.proxy_url()?,
371 auth_info: loader.user,
372 tls_server_name: loader.cluster.tls_server_name,
373 headers: Vec::new(),
374 default_retry: true,
375 })
376 }
377
378 pub fn apply_debug_overrides(&mut self) {
389 if let Ok(impersonate_user) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_USER") {
391 tracing::warn!(?impersonate_user, "impersonating user");
392 self.auth_info.impersonate = Some(impersonate_user);
393 }
394 if let Ok(impersonate_groups) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_GROUP") {
395 let impersonate_groups = impersonate_groups.split(',').map(str::to_string).collect();
396 tracing::warn!(?impersonate_groups, "impersonating groups");
397 self.auth_info.impersonate_groups = Some(impersonate_groups);
398 }
399 if let Ok(url) = std::env::var("KUBE_RS_DEBUG_OVERRIDE_URL") {
400 tracing::warn!(?url, "overriding cluster URL");
401 match url.parse() {
402 Ok(uri) => {
403 self.cluster_url = uri;
404 }
405 Err(err) => {
406 tracing::warn!(
407 ?url,
408 error = &err as &dyn std::error::Error,
409 "failed to parse override cluster URL, ignoring"
410 );
411 }
412 }
413 }
414 }
415
416 pub(crate) fn identity_pem(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
418 self.auth_info.identity_pem()
419 }
420}
421
422pub(crate) fn certs(data: &[u8]) -> Result<Vec<Vec<u8>>, pem::PemError> {
423 Ok(pem::parse_many(data)?
424 .into_iter()
425 .filter_map(|p| {
426 if p.tag() == "CERTIFICATE" {
427 Some(p.into_contents())
428 } else {
429 None
430 }
431 })
432 .collect::<Vec<_>>())
433}
434
435impl TryFrom<Kubeconfig> for Config {
436 type Error = KubeconfigError;
437
438 fn try_from(kubeconfig: Kubeconfig) -> Result<Self, KubeconfigError> {
439 let loader = ConfigLoader::try_from(kubeconfig)?;
440 Self::new_from_loader(loader)
441 }
442}
443
444const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
446const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);
447
448pub use file_config::{
450 AuthInfo, AuthProviderConfig, Cluster, Context, ExecAuthCluster, ExecConfig, ExecInteractiveMode,
451 Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences,
452};
453
454#[cfg(test)]
455mod tests {
456 #[cfg(not(feature = "client"))] #[tokio::test]
458 async fn config_loading_on_small_feature_set() {
459 use super::Config;
460 let cfgraw = r#"
461 apiVersion: v1
462 clusters:
463 - cluster:
464 certificate-authority-data: aGVsbG8K
465 server: https://0.0.0.0:6443
466 name: k3d-test
467 contexts:
468 - context:
469 cluster: k3d-test
470 user: admin@k3d-test
471 name: k3d-test
472 current-context: k3d-test
473 kind: Config
474 preferences: {}
475 users:
476 - name: admin@k3d-test
477 user:
478 client-certificate-data: aGVsbG8K
479 client-key-data: aGVsbG8K
480 "#;
481 let file = tempfile::NamedTempFile::new().expect("create config tempfile");
482 std::fs::write(file.path(), cfgraw).unwrap();
483 std::env::set_var("KUBECONFIG", file.path());
484 let kubeconfig = Config::infer().await.unwrap();
485 assert_eq!(kubeconfig.cluster_url, "https://0.0.0.0:6443/");
486 }
487}