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
19use file_loader::ConfigLoader;
20pub use file_loader::KubeConfigOptions;
21pub use incluster_config::Error as InClusterError;
22
23#[derive(Error, Debug)]
25#[error("failed to infer config: in-cluster: ({in_cluster}), kubeconfig: ({kubeconfig})")]
26pub struct InferConfigError {
27 in_cluster: InClusterError,
28 #[source]
30 kubeconfig: KubeconfigError,
31}
32
33#[derive(Error, Debug)]
35pub enum KubeconfigError {
36 #[error("failed to determine current context")]
38 CurrentContextNotSet,
39
40 #[error("kubeconfigs with mismatching kind cannot be merged")]
42 KindMismatch,
43
44 #[error("kubeconfigs with mismatching api version cannot be merged")]
46 ApiVersionMismatch,
47
48 #[error("failed to load current context: {0}")]
50 LoadContext(String),
51
52 #[error("failed to load the cluster of context: {0}")]
54 LoadClusterOfContext(String),
55
56 #[error("failed to find the path of kubeconfig")]
58 FindPath,
59
60 #[error("failed to read kubeconfig from '{1:?}': {0}")]
62 ReadConfig(#[source] std::io::Error, PathBuf),
63
64 #[error("failed to parse kubeconfig YAML: {0}")]
66 Parse(Box<serde_saphyr::Error>),
67
68 #[error("cluster url is missing on selected cluster")]
70 MissingClusterUrl,
71
72 #[error("failed to parse cluster url: {0}")]
74 ParseClusterUrl(#[source] http::uri::InvalidUri),
75
76 #[error("failed to parse proxy url: {0}")]
78 ParseProxyUrl(#[source] http::uri::InvalidUri),
79
80 #[error("failed to load certificate authority")]
82 LoadCertificateAuthority(#[source] LoadDataError),
83
84 #[error("failed to load client certificate")]
86 LoadClientCertificate(#[source] LoadDataError),
87
88 #[error("failed to load client key")]
90 LoadClientKey(#[source] LoadDataError),
91
92 #[error("failed to parse PEM-encoded certificates: {0}")]
94 ParseCertificates(#[source] pem::PemError),
95}
96
97#[derive(Debug, Error)]
99pub enum LoadDataError {
100 #[error("failed to decode base64 data: {0}")]
102 DecodeBase64(#[source] base64::DecodeError),
103
104 #[error("failed to read file '{1:?}': {0}")]
106 ReadFile(#[source] std::io::Error, PathBuf),
107
108 #[error("no base64 data or file")]
110 NoBase64DataOrFile,
111}
112
113#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
126#[derive(Debug, Clone)]
127#[non_exhaustive]
128pub struct Config {
129 pub cluster_url: http::Uri,
131 pub default_namespace: String,
133 pub root_cert: Option<Vec<Vec<u8>>>,
135 pub root_cert_file: Option<PathBuf>,
144 pub connect_timeout: Option<std::time::Duration>,
148 pub read_timeout: Option<std::time::Duration>,
156 pub write_timeout: Option<std::time::Duration>,
160 pub accept_invalid_certs: bool,
162 pub auth_info: AuthInfo,
164 pub disable_compression: bool,
166 pub proxy_url: Option<http::Uri>,
168 pub tls_server_name: Option<String>,
172 pub headers: Vec<(HeaderName, HeaderValue)>,
174 pub default_retry: bool,
176}
177
178impl Config {
179 pub fn new(cluster_url: http::Uri) -> Self {
185 Self {
186 cluster_url,
187 default_namespace: String::from("default"),
188 root_cert: None,
189 root_cert_file: None,
190 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
191 read_timeout: None,
192 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
193 accept_invalid_certs: false,
194 auth_info: AuthInfo::default(),
195 disable_compression: false,
196 proxy_url: None,
197 tls_server_name: None,
198 headers: Vec::new(),
199 default_retry: true,
200 }
201 }
202
203 pub async fn infer() -> Result<Self, InferConfigError> {
213 let mut config = match Self::from_kubeconfig(&KubeConfigOptions::default()).await {
214 Err(kubeconfig_err) => {
215 tracing::trace!(
216 error = &kubeconfig_err as &dyn std::error::Error,
217 "no local config found, falling back to local in-cluster config"
218 );
219
220 Self::incluster().map_err(|in_cluster| InferConfigError {
221 in_cluster,
222 kubeconfig: kubeconfig_err,
223 })?
224 }
225 Ok(success) => success,
226 };
227 config.apply_debug_overrides();
228 Ok(config)
229 }
230
231 pub fn incluster() -> Result<Self, InClusterError> {
234 Self::incluster_env()
235 }
236
237 pub fn incluster_env() -> Result<Self, InClusterError> {
246 let uri = incluster_config::try_kube_from_env()?;
247 Self::incluster_with_uri(uri)
248 }
249
250 pub fn incluster_dns() -> Result<Self, InClusterError> {
260 Self::incluster_with_uri(incluster_config::kube_dns())
261 }
262
263 fn incluster_with_uri(cluster_url: http::uri::Uri) -> Result<Self, InClusterError> {
264 let default_namespace = incluster_config::load_default_ns()?;
265 let root_cert = incluster_config::load_cert()?;
266
267 Ok(Self {
268 cluster_url,
269 default_namespace,
270 root_cert: Some(root_cert),
271 root_cert_file: Some(PathBuf::from(incluster_config::cert_file())),
272 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
273 read_timeout: None,
274 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
275 accept_invalid_certs: false,
276 auth_info: AuthInfo {
277 token_file: Some(incluster_config::token_file()),
278 ..Default::default()
279 },
280 disable_compression: false,
281 proxy_url: None,
282 tls_server_name: None,
283 headers: Vec::new(),
284 default_retry: true,
285 })
286 }
287
288 pub async fn from_kubeconfig(options: &KubeConfigOptions) -> Result<Self, KubeconfigError> {
294 let loader = ConfigLoader::new_from_options(options).await?;
295 Self::new_from_loader(loader)
296 }
297
298 pub async fn from_custom_kubeconfig(
302 kubeconfig: Kubeconfig,
303 options: &KubeConfigOptions,
304 ) -> Result<Self, KubeconfigError> {
305 let loader = ConfigLoader::new_from_kubeconfig(kubeconfig, options).await?;
306 Self::new_from_loader(loader)
307 }
308
309 fn new_from_loader(loader: ConfigLoader) -> Result<Self, KubeconfigError> {
310 let cluster_url = loader
311 .cluster
312 .server
313 .clone()
314 .ok_or(KubeconfigError::MissingClusterUrl)?
315 .parse::<http::Uri>()
316 .map_err(KubeconfigError::ParseClusterUrl)?;
317
318 let default_namespace = loader
319 .current_context
320 .namespace
321 .clone()
322 .unwrap_or_else(|| String::from("default"));
323
324 let accept_invalid_certs = loader.cluster.insecure_skip_tls_verify.unwrap_or(false);
325 let disable_compression = loader.cluster.disable_compression.unwrap_or(false);
326
327 let mut root_cert = None;
328
329 if let Some(ca_bundle) = loader.ca_bundle()? {
330 root_cert = Some(ca_bundle);
331 }
332
333 Ok(Self {
334 cluster_url,
335 default_namespace,
336 root_cert,
337 root_cert_file: None,
338 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
339 read_timeout: None,
340 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
341 accept_invalid_certs,
342 disable_compression,
343 proxy_url: loader.proxy_url()?,
344 auth_info: loader.user,
345 tls_server_name: loader.cluster.tls_server_name,
346 headers: Vec::new(),
347 default_retry: true,
348 })
349 }
350
351 pub fn apply_debug_overrides(&mut self) {
362 if let Ok(impersonate_user) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_USER") {
364 tracing::warn!(?impersonate_user, "impersonating user");
365 self.auth_info.impersonate = Some(impersonate_user);
366 }
367 if let Ok(impersonate_groups) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_GROUP") {
368 let impersonate_groups = impersonate_groups.split(',').map(str::to_string).collect();
369 tracing::warn!(?impersonate_groups, "impersonating groups");
370 self.auth_info.impersonate_groups = Some(impersonate_groups);
371 }
372 if let Ok(url) = std::env::var("KUBE_RS_DEBUG_OVERRIDE_URL") {
373 tracing::warn!(?url, "overriding cluster URL");
374 match url.parse() {
375 Ok(uri) => {
376 self.cluster_url = uri;
377 }
378 Err(err) => {
379 tracing::warn!(
380 ?url,
381 error = &err as &dyn std::error::Error,
382 "failed to parse override cluster URL, ignoring"
383 );
384 }
385 }
386 }
387 }
388
389 pub(crate) fn identity_pem(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
391 self.auth_info.identity_pem()
392 }
393}
394
395pub(crate) fn certs(data: &[u8]) -> Result<Vec<Vec<u8>>, pem::PemError> {
396 Ok(pem::parse_many(data)?
397 .into_iter()
398 .filter_map(|p| {
399 if p.tag() == "CERTIFICATE" {
400 Some(p.into_contents())
401 } else {
402 None
403 }
404 })
405 .collect::<Vec<_>>())
406}
407
408impl TryFrom<Kubeconfig> for Config {
409 type Error = KubeconfigError;
410
411 fn try_from(kubeconfig: Kubeconfig) -> Result<Self, KubeconfigError> {
412 let loader = ConfigLoader::try_from(kubeconfig)?;
413 Self::new_from_loader(loader)
414 }
415}
416
417const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
419const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);
420
421pub use file_config::{
423 AuthInfo, AuthProviderConfig, Cluster, Context, ExecAuthCluster, ExecConfig, ExecInteractiveMode,
424 Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences,
425};
426
427#[cfg(test)]
428mod tests {
429 #[cfg(not(feature = "client"))] #[tokio::test]
431 async fn config_loading_on_small_feature_set() {
432 use super::Config;
433 let cfgraw = r#"
434 apiVersion: v1
435 clusters:
436 - cluster:
437 certificate-authority-data: aGVsbG8K
438 server: https://0.0.0.0:6443
439 name: k3d-test
440 contexts:
441 - context:
442 cluster: k3d-test
443 user: admin@k3d-test
444 name: k3d-test
445 current-context: k3d-test
446 kind: Config
447 preferences: {}
448 users:
449 - name: admin@k3d-test
450 user:
451 client-certificate-data: aGVsbG8K
452 client-key-data: aGVsbG8K
453 "#;
454 let file = tempfile::NamedTempFile::new().expect("create config tempfile");
455 std::fs::write(file.path(), cfgraw).unwrap();
456 std::env::set_var("KUBECONFIG", file.path());
457 let kubeconfig = Config::infer().await.unwrap();
458 assert_eq!(kubeconfig.cluster_url, "https://0.0.0.0:6443/");
459 }
460}