Skip to main content

kube_client/config/
mod.rs

1//! Kubernetes configuration objects.
2//!
3//! Reads locally from `$KUBECONFIG` or `~/.kube/config`,
4//! and in-cluster from the [pod environment](https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/#accessing-the-api-from-within-a-pod).
5//!
6//! # Usage
7//! The [`Config`] has several constructors plus logic to infer environment.
8//!
9//! Unless you have issues, prefer using [`Config::infer`], and pass it to a [`Client`][crate::Client].
10use 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/// Failed to infer config
28#[derive(Error, Debug)]
29#[error("failed to infer config: in-cluster: ({in_cluster}), kubeconfig: ({kubeconfig})")]
30pub struct InferConfigError {
31    in_cluster: Box<InClusterError>,
32    // We can only pick one source, but the kubeconfig failure is more likely to be a user error
33    #[source]
34    kubeconfig: KubeconfigError,
35}
36
37/// Possible errors when loading kubeconfig
38#[derive(Error, Debug)]
39pub enum KubeconfigError {
40    /// Failed to determine current context
41    #[error("failed to determine current context")]
42    CurrentContextNotSet,
43
44    /// Kubeconfigs with mismatching kind cannot be merged
45    #[error("kubeconfigs with mismatching kind cannot be merged")]
46    KindMismatch,
47
48    /// Kubeconfigs with mismatching api version cannot be merged
49    #[error("kubeconfigs with mismatching api version cannot be merged")]
50    ApiVersionMismatch,
51
52    /// Failed to load current context
53    #[error("failed to load current context: {0}")]
54    LoadContext(String),
55
56    /// Failed to load the cluster of context
57    #[error("failed to load the cluster of context: {0}")]
58    LoadClusterOfContext(String),
59
60    /// Failed to find the path of kubeconfig
61    #[error("failed to find the path of kubeconfig")]
62    FindPath,
63
64    /// Failed to read kubeconfig
65    #[error("failed to read kubeconfig from '{1:?}': {0}")]
66    ReadConfig(#[source] std::io::Error, PathBuf),
67
68    /// Failed to parse kubeconfig YAML
69    #[error("failed to parse kubeconfig YAML: {0}")]
70    Parse(Box<serde_saphyr::Error>),
71
72    /// Cluster url is missing on selected cluster
73    #[error("cluster url is missing on selected cluster")]
74    MissingClusterUrl,
75
76    /// Failed to parse cluster url
77    #[error("failed to parse cluster url: {0}")]
78    ParseClusterUrl(#[source] http::uri::InvalidUri),
79
80    /// Failed to parse proxy url
81    #[error("failed to parse proxy url: {0}")]
82    ParseProxyUrl(#[source] http::uri::InvalidUri),
83
84    /// Failed to load certificate authority
85    #[error("failed to load certificate authority: {0}")]
86    LoadCertificateAuthority(#[source] LoadDataError),
87
88    /// Failed to load client certificate
89    #[error("failed to load client certificate: {0}")]
90    LoadClientCertificate(#[source] LoadDataError),
91
92    /// Failed to load client key
93    #[error("failed to load client key: {0}")]
94    LoadClientKey(#[source] LoadDataError),
95
96    /// Failed to parse PEM-encoded certificates
97    #[error("failed to parse PEM-encoded certificates: {0}")]
98    ParseCertificates(#[source] pem::PemError),
99
100    /// Failed to parse NO_PROXY/no_proxy env var
101    #[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/// Errors from loading data from a base64 string or a file
107#[derive(Debug, Error)]
108pub enum LoadDataError {
109    /// Failed to decode base64 data
110    #[error("failed to decode base64 data: {0}")]
111    DecodeBase64(#[source] base64::DecodeError),
112
113    /// Failed to read file
114    #[error("failed to read file '{1:?}': {0}")]
115    ReadFile(#[source] std::io::Error, PathBuf),
116
117    /// No base64 data or file path was provided
118    #[error("no base64 data or file")]
119    NoBase64DataOrFile,
120}
121
122/// Configuration object for accessing a Kubernetes cluster
123///
124/// The configurable parameters for connecting like cluster URL, default namespace, root certificates, and timeouts.
125/// Normally created implicitly through [`Config::infer`] or [`Client::try_default`](crate::Client::try_default).
126///
127/// # Usage
128/// Construct a [`Config`] instance by using one of the many constructors.
129///
130/// Prefer [`Config::infer`] unless you have particular issues, and avoid manually managing
131/// the data in this struct unless you have particular needs. It exists to be consumed by the [`Client`][crate::Client].
132///
133/// If you are looking to parse the kubeconfig found in a user's home directory see [`Kubeconfig`].
134#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
135#[derive(Debug, Clone)]
136#[non_exhaustive]
137pub struct Config {
138    /// The configured cluster url
139    pub cluster_url: http::Uri,
140    /// The configured default namespace
141    pub default_namespace: String,
142    /// The configured root certificate
143    pub root_cert: Option<Vec<Vec<u8>>>,
144    /// Path to the root certificate bundle file.
145    ///
146    /// When set and the `rustls-tls` feature is enabled, the file is re-read
147    /// periodically (~60 s) to pick up CA rotation, mirroring how
148    /// `token_file` is reloaded. This takes precedence over `root_cert` for
149    /// server certificate verification.
150    ///
151    /// Set automatically by [`Config::incluster`].
152    pub root_cert_file: Option<PathBuf>,
153    /// Set the timeout for connecting to the Kubernetes API.
154    ///
155    /// A value of `None` means no timeout
156    pub connect_timeout: Option<std::time::Duration>,
157    /// Set the timeout for the Kubernetes API response.
158    ///
159    /// A value of `None` means no timeout.
160    ///
161    /// Defaults to `None` to avoid breaking long-lived connections such as
162    /// exec, attach and port-forward sessions.  Watch streams are protected
163    /// by a watcher-level idle timeout instead.
164    ///
165    /// If you set this, consider also turning on [`Self::reset_reader_on_write`].
166    /// Otherwise the time a pooled connection sat idle before being handed out
167    /// (up to 90 s) counts against this timeout for the next response on that
168    /// connection.
169    pub read_timeout: Option<std::time::Duration>,
170    /// Set the timeout for the Kubernetes API request.
171    ///
172    /// A value of `None` means no timeout
173    pub write_timeout: Option<std::time::Duration>,
174    /// Whether to make every completed write reset the read timeout. The timeout then counts from
175    /// when a request is sent, not from when its pooled connection went idle (which is potentially a
176    /// long time before the connection was handed out for a request).
177    ///
178    /// Has an effect only when [`Self::read_timeout`] is set.
179    ///
180    /// The tradeoff is that this makes things like kube's 60 s WebSocket pings also reset it on
181    /// exec/attach, and thus the read timeout no longer catches a peer that stops answering while the
182    /// client keeps writing.
183    pub reset_reader_on_write: bool,
184    /// Whether to accept invalid certificates
185    pub accept_invalid_certs: bool,
186    /// Stores information to tell the cluster who you are.
187    pub auth_info: AuthInfo,
188    /// Whether to disable compression (would only have an effect when the `gzip` feature is enabled)
189    pub disable_compression: bool,
190    /// Optional proxy URL. Proxy support requires the `socks5` feature.
191    pub proxy_url: Option<http::Uri>,
192    /// If set, apiserver certificate will be validated to contain this string
193    ///
194    /// If not set, the `cluster_url` is used instead
195    pub tls_server_name: Option<String>,
196    /// Headers to pass with every request.
197    pub headers: Vec<(HeaderName, HeaderValue)>,
198    /// Whether to enable default retrying requests on transient failures (429, 503, 504).
199    pub default_retry: bool,
200}
201
202impl Config {
203    /// Construct a new config where only the `cluster_url` is set by the user.
204    /// and everything else receives a default value.
205    ///
206    /// Most likely you want to use [`Config::infer`] to infer the config from
207    /// the environment.
208    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    /// Infer a Kubernetes client configuration.
229    ///
230    /// First, a user's kubeconfig is loaded from `KUBECONFIG` or
231    /// `~/.kube/config`. If that fails, an in-cluster config is loaded via
232    /// [`Config::incluster`]. If inference from both sources fails, then an
233    /// error is returned.
234    ///
235    /// [`Config::apply_debug_overrides`] is used to augment the loaded
236    /// configuration based on the environment.
237    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    /// Load an in-cluster Kubernetes client configuration using
257    /// [`Config::incluster_env`].
258    pub fn incluster() -> Result<Self, InClusterError> {
259        Self::incluster_env()
260    }
261
262    /// Load an in-cluster config using the `KUBERNETES_SERVICE_HOST` and
263    /// `KUBERNETES_SERVICE_PORT` environment variables.
264    ///
265    /// A service account's token must be available in
266    /// `/var/run/secrets/kubernetes.io/serviceaccount/`.
267    ///
268    /// This method matches the behavior of the official Kubernetes client
269    /// libraries and is the default for both TLS stacks.
270    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    /// Load an in-cluster config using the API server at
276    /// `https://kubernetes.default.svc`.
277    ///
278    /// A service account's token must be available in
279    /// `/var/run/secrets/kubernetes.io/serviceaccount/`.
280    ///
281    /// This behavior does not match that of the official Kubernetes clients,
282    /// but can be used as a consistent entrypoint in many clusters.
283    /// See <https://github.com/kube-rs/kube/issues/1003> for more info.
284    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    /// Create configuration from the default local config file
315    ///
316    /// This will respect the `$KUBECONFIG` evar, but otherwise default to `~/.kube/config`.
317    /// You can also customize what context/cluster/user you want to use here,
318    /// but it will default to the current-context.
319    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    /// Create configuration from a [`Kubeconfig`] struct
325    ///
326    /// This bypasses kube's normal config parsing to obtain custom functionality.
327    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    /// Override configuration based on environment variables
379    ///
380    /// This is only intended for use as a debugging aid, and the specific variables and their behaviour
381    /// should **not** be considered stable across releases.
382    ///
383    /// Currently, the following overrides are supported:
384    ///
385    /// - `KUBE_RS_DEBUG_IMPERSONATE_USER`: A Kubernetes user to impersonate, for example: `system:serviceaccount:default:foo` will impersonate the `ServiceAccount` `foo` in the `Namespace` `default`
386    /// - `KUBE_RS_DEBUG_IMPERSONATE_GROUP`: A Kubernetes group to impersonate, multiple groups may be specified by separating them with commas
387    /// - `KUBE_RS_DEBUG_OVERRIDE_URL`: A Kubernetes cluster URL to use rather than the one specified in the config, useful for proxying traffic through `kubectl proxy`
388    pub fn apply_debug_overrides(&mut self) {
389        // Log these overrides loudly, to emphasize that this is only a debugging aid, and should not be relied upon in production
390        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    /// Client certificate and private key in PEM.
417    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
444// https://github.com/kube-rs/kube/issues/146#issuecomment-590924397
445const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
446const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);
447
448// Expose raw config structs
449pub 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"))] // want to ensure this works without client features
457    #[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}