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(#[source] serde_yaml::Error),
67
68 #[error("the structure of the parsed kubeconfig is invalid: {0}")]
70 InvalidStructure(#[source] serde_yaml::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")]
86 LoadCertificateAuthority(#[source] LoadDataError),
87
88 #[error("failed to load client certificate")]
90 LoadClientCertificate(#[source] LoadDataError),
91
92 #[error("failed to load client key")]
94 LoadClientKey(#[source] LoadDataError),
95
96 #[error("failed to parse PEM-encoded certificates: {0}")]
98 ParseCertificates(#[source] pem::PemError),
99}
100
101#[derive(Debug, Error)]
103pub enum LoadDataError {
104 #[error("failed to decode base64 data: {0}")]
106 DecodeBase64(#[source] base64::DecodeError),
107
108 #[error("failed to read file '{1:?}': {0}")]
110 ReadFile(#[source] std::io::Error, PathBuf),
111
112 #[error("no base64 data or file")]
114 NoBase64DataOrFile,
115}
116
117#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
130#[derive(Debug, Clone)]
131pub struct Config {
132 pub cluster_url: http::Uri,
134 pub default_namespace: String,
136 pub root_cert: Option<Vec<Vec<u8>>>,
138 pub connect_timeout: Option<std::time::Duration>,
142 pub read_timeout: Option<std::time::Duration>,
146 pub write_timeout: Option<std::time::Duration>,
150 pub accept_invalid_certs: bool,
152 pub auth_info: AuthInfo,
154 pub disable_compression: bool,
156 pub proxy_url: Option<http::Uri>,
158 pub tls_server_name: Option<String>,
162 pub headers: Vec<(HeaderName, HeaderValue)>,
164}
165
166impl Config {
167 pub fn new(cluster_url: http::Uri) -> Self {
173 Self {
174 cluster_url,
175 default_namespace: String::from("default"),
176 root_cert: None,
177 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
178 read_timeout: Some(DEFAULT_READ_TIMEOUT),
179 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
180 accept_invalid_certs: false,
181 auth_info: AuthInfo::default(),
182 disable_compression: false,
183 proxy_url: None,
184 tls_server_name: None,
185 headers: Vec::new(),
186 }
187 }
188
189 pub async fn infer() -> Result<Self, InferConfigError> {
199 let mut config = match Self::from_kubeconfig(&KubeConfigOptions::default()).await {
200 Err(kubeconfig_err) => {
201 tracing::trace!(
202 error = &kubeconfig_err as &dyn std::error::Error,
203 "no local config found, falling back to local in-cluster config"
204 );
205
206 Self::incluster().map_err(|in_cluster| InferConfigError {
207 in_cluster,
208 kubeconfig: kubeconfig_err,
209 })?
210 }
211 Ok(success) => success,
212 };
213 config.apply_debug_overrides();
214 Ok(config)
215 }
216
217 pub fn incluster() -> Result<Self, InClusterError> {
220 Self::incluster_env()
221 }
222
223 pub fn incluster_env() -> Result<Self, InClusterError> {
232 let uri = incluster_config::try_kube_from_env()?;
233 Self::incluster_with_uri(uri)
234 }
235
236 pub fn incluster_dns() -> Result<Self, InClusterError> {
246 Self::incluster_with_uri(incluster_config::kube_dns())
247 }
248
249 fn incluster_with_uri(cluster_url: http::uri::Uri) -> Result<Self, InClusterError> {
250 let default_namespace = incluster_config::load_default_ns()?;
251 let root_cert = incluster_config::load_cert()?;
252
253 Ok(Self {
254 cluster_url,
255 default_namespace,
256 root_cert: Some(root_cert),
257 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
258 read_timeout: Some(DEFAULT_READ_TIMEOUT),
259 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
260 accept_invalid_certs: false,
261 auth_info: AuthInfo {
262 token_file: Some(incluster_config::token_file()),
263 ..Default::default()
264 },
265 disable_compression: false,
266 proxy_url: None,
267 tls_server_name: None,
268 headers: Vec::new(),
269 })
270 }
271
272 pub async fn from_kubeconfig(options: &KubeConfigOptions) -> Result<Self, KubeconfigError> {
278 let loader = ConfigLoader::new_from_options(options).await?;
279 Self::new_from_loader(loader).await
280 }
281
282 pub async fn from_custom_kubeconfig(
286 kubeconfig: Kubeconfig,
287 options: &KubeConfigOptions,
288 ) -> Result<Self, KubeconfigError> {
289 let loader = ConfigLoader::new_from_kubeconfig(kubeconfig, options).await?;
290 Self::new_from_loader(loader).await
291 }
292
293 async fn new_from_loader(loader: ConfigLoader) -> Result<Self, KubeconfigError> {
294 let cluster_url = loader
295 .cluster
296 .server
297 .clone()
298 .ok_or(KubeconfigError::MissingClusterUrl)?
299 .parse::<http::Uri>()
300 .map_err(KubeconfigError::ParseClusterUrl)?;
301
302 let default_namespace = loader
303 .current_context
304 .namespace
305 .clone()
306 .unwrap_or_else(|| String::from("default"));
307
308 let accept_invalid_certs = loader.cluster.insecure_skip_tls_verify.unwrap_or(false);
309 let disable_compression = loader.cluster.disable_compression.unwrap_or(false);
310
311 let mut root_cert = None;
312
313 if let Some(ca_bundle) = loader.ca_bundle()? {
314 root_cert = Some(ca_bundle);
315 }
316
317 Ok(Self {
318 cluster_url,
319 default_namespace,
320 root_cert,
321 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
322 read_timeout: Some(DEFAULT_READ_TIMEOUT),
323 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
324 accept_invalid_certs,
325 disable_compression,
326 proxy_url: loader.proxy_url()?,
327 auth_info: loader.user,
328 tls_server_name: loader.cluster.tls_server_name,
329 headers: Vec::new(),
330 })
331 }
332
333 pub fn apply_debug_overrides(&mut self) {
344 if let Ok(impersonate_user) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_USER") {
346 tracing::warn!(?impersonate_user, "impersonating user");
347 self.auth_info.impersonate = Some(impersonate_user);
348 }
349 if let Ok(impersonate_groups) = std::env::var("KUBE_RS_DEBUG_IMPERSONATE_GROUP") {
350 let impersonate_groups = impersonate_groups.split(',').map(str::to_string).collect();
351 tracing::warn!(?impersonate_groups, "impersonating groups");
352 self.auth_info.impersonate_groups = Some(impersonate_groups);
353 }
354 if let Ok(url) = std::env::var("KUBE_RS_DEBUG_OVERRIDE_URL") {
355 tracing::warn!(?url, "overriding cluster URL");
356 match url.parse() {
357 Ok(uri) => {
358 self.cluster_url = uri;
359 }
360 Err(err) => {
361 tracing::warn!(
362 ?url,
363 error = &err as &dyn std::error::Error,
364 "failed to parse override cluster URL, ignoring"
365 );
366 }
367 }
368 }
369 }
370
371 pub(crate) fn identity_pem(&self) -> Option<Vec<u8>> {
373 self.auth_info.identity_pem().ok()
374 }
375}
376
377fn certs(data: &[u8]) -> Result<Vec<Vec<u8>>, pem::PemError> {
378 Ok(pem::parse_many(data)?
379 .into_iter()
380 .filter_map(|p| {
381 if p.tag() == "CERTIFICATE" {
382 Some(p.into_contents())
383 } else {
384 None
385 }
386 })
387 .collect::<Vec<_>>())
388}
389
390const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
392const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(295);
393const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);
394
395pub use file_config::{
397 AuthInfo, AuthProviderConfig, Cluster, Context, ExecAuthCluster, ExecConfig, ExecInteractiveMode,
398 Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext, NamedExtension, Preferences,
399};
400
401#[cfg(test)]
402mod tests {
403 #[cfg(not(feature = "client"))] #[tokio::test]
405 async fn config_loading_on_small_feature_set() {
406 use super::Config;
407 let cfgraw = r#"
408 apiVersion: v1
409 clusters:
410 - cluster:
411 certificate-authority-data: aGVsbG8K
412 server: https://0.0.0.0:6443
413 name: k3d-test
414 contexts:
415 - context:
416 cluster: k3d-test
417 user: admin@k3d-test
418 name: k3d-test
419 current-context: k3d-test
420 kind: Config
421 preferences: {}
422 users:
423 - name: admin@k3d-test
424 user:
425 client-certificate-data: aGVsbG8K
426 client-key-data: aGVsbG8K
427 "#;
428 let file = tempfile::NamedTempFile::new().expect("create config tempfile");
429 std::fs::write(file.path(), cfgraw).unwrap();
430 std::env::set_var("KUBECONFIG", file.path());
431 let kubeconfig = Config::infer().await.unwrap();
432 assert_eq!(kubeconfig.cluster_url, "https://0.0.0.0:6443/");
433 }
434}