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
19use file_loader::ConfigLoader;
20pub use file_loader::KubeConfigOptions;
21pub use incluster_config::Error as InClusterError;
22
23/// Failed to infer config
24#[derive(Error, Debug)]
25#[error("failed to infer config: in-cluster: ({in_cluster}), kubeconfig: ({kubeconfig})")]
26pub struct InferConfigError {
27    in_cluster: InClusterError,
28    // We can only pick one source, but the kubeconfig failure is more likely to be a user error
29    #[source]
30    kubeconfig: KubeconfigError,
31}
32
33/// Possible errors when loading kubeconfig
34#[derive(Error, Debug)]
35pub enum KubeconfigError {
36    /// Failed to determine current context
37    #[error("failed to determine current context")]
38    CurrentContextNotSet,
39
40    /// Kubeconfigs with mismatching kind cannot be merged
41    #[error("kubeconfigs with mismatching kind cannot be merged")]
42    KindMismatch,
43
44    /// Kubeconfigs with mismatching api version cannot be merged
45    #[error("kubeconfigs with mismatching api version cannot be merged")]
46    ApiVersionMismatch,
47
48    /// Failed to load current context
49    #[error("failed to load current context: {0}")]
50    LoadContext(String),
51
52    /// Failed to load the cluster of context
53    #[error("failed to load the cluster of context: {0}")]
54    LoadClusterOfContext(String),
55
56    /// Failed to find the path of kubeconfig
57    #[error("failed to find the path of kubeconfig")]
58    FindPath,
59
60    /// Failed to read kubeconfig
61    #[error("failed to read kubeconfig from '{1:?}': {0}")]
62    ReadConfig(#[source] std::io::Error, PathBuf),
63
64    /// Failed to parse kubeconfig YAML
65    #[error("failed to parse kubeconfig YAML: {0}")]
66    Parse(Box<serde_saphyr::Error>),
67
68    /// Cluster url is missing on selected cluster
69    #[error("cluster url is missing on selected cluster")]
70    MissingClusterUrl,
71
72    /// Failed to parse cluster url
73    #[error("failed to parse cluster url: {0}")]
74    ParseClusterUrl(#[source] http::uri::InvalidUri),
75
76    /// Failed to parse proxy url
77    #[error("failed to parse proxy url: {0}")]
78    ParseProxyUrl(#[source] http::uri::InvalidUri),
79
80    /// Failed to load certificate authority
81    #[error("failed to load certificate authority")]
82    LoadCertificateAuthority(#[source] LoadDataError),
83
84    /// Failed to load client certificate
85    #[error("failed to load client certificate")]
86    LoadClientCertificate(#[source] LoadDataError),
87
88    /// Failed to load client key
89    #[error("failed to load client key")]
90    LoadClientKey(#[source] LoadDataError),
91
92    /// Failed to parse PEM-encoded certificates
93    #[error("failed to parse PEM-encoded certificates: {0}")]
94    ParseCertificates(#[source] pem::PemError),
95}
96
97/// Errors from loading data from a base64 string or a file
98#[derive(Debug, Error)]
99pub enum LoadDataError {
100    /// Failed to decode base64 data
101    #[error("failed to decode base64 data: {0}")]
102    DecodeBase64(#[source] base64::DecodeError),
103
104    /// Failed to read file
105    #[error("failed to read file '{1:?}': {0}")]
106    ReadFile(#[source] std::io::Error, PathBuf),
107
108    /// No base64 data or file path was provided
109    #[error("no base64 data or file")]
110    NoBase64DataOrFile,
111}
112
113/// Configuration object for accessing a Kubernetes cluster
114///
115/// The configurable parameters for connecting like cluster URL, default namespace, root certificates, and timeouts.
116/// Normally created implicitly through [`Config::infer`] or [`Client::try_default`](crate::Client::try_default).
117///
118/// # Usage
119/// Construct a [`Config`] instance by using one of the many constructors.
120///
121/// Prefer [`Config::infer`] unless you have particular issues, and avoid manually managing
122/// the data in this struct unless you have particular needs. It exists to be consumed by the [`Client`][crate::Client].
123///
124/// If you are looking to parse the kubeconfig found in a user's home directory see [`Kubeconfig`].
125#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
126#[derive(Debug, Clone)]
127#[non_exhaustive]
128pub struct Config {
129    /// The configured cluster url
130    pub cluster_url: http::Uri,
131    /// The configured default namespace
132    pub default_namespace: String,
133    /// The configured root certificate
134    pub root_cert: Option<Vec<Vec<u8>>>,
135    /// Path to the root certificate bundle file.
136    ///
137    /// When set and the `rustls-tls` feature is enabled, the file is re-read
138    /// periodically (~60 s) to pick up CA rotation, mirroring how
139    /// `token_file` is reloaded. This takes precedence over `root_cert` for
140    /// server certificate verification.
141    ///
142    /// Set automatically by [`Config::incluster`].
143    pub root_cert_file: Option<PathBuf>,
144    /// Set the timeout for connecting to the Kubernetes API.
145    ///
146    /// A value of `None` means no timeout
147    pub connect_timeout: Option<std::time::Duration>,
148    /// Set the timeout for the Kubernetes API response.
149    ///
150    /// A value of `None` means no timeout.
151    ///
152    /// Defaults to `None` to avoid breaking long-lived connections such as
153    /// exec, attach and port-forward sessions.  Watch streams are protected
154    /// by a watcher-level idle timeout instead.
155    pub read_timeout: Option<std::time::Duration>,
156    /// Set the timeout for the Kubernetes API request.
157    ///
158    /// A value of `None` means no timeout
159    pub write_timeout: Option<std::time::Duration>,
160    /// Whether to accept invalid certificates
161    pub accept_invalid_certs: bool,
162    /// Stores information to tell the cluster who you are.
163    pub auth_info: AuthInfo,
164    /// Whether to disable compression (would only have an effect when the `gzip` feature is enabled)
165    pub disable_compression: bool,
166    /// Optional proxy URL. Proxy support requires the `socks5` feature.
167    pub proxy_url: Option<http::Uri>,
168    /// If set, apiserver certificate will be validated to contain this string
169    ///
170    /// If not set, the `cluster_url` is used instead
171    pub tls_server_name: Option<String>,
172    /// Headers to pass with every request.
173    pub headers: Vec<(HeaderName, HeaderValue)>,
174    /// Whether to enable default retrying requests on transient failures (429, 503, 504).
175    pub default_retry: bool,
176}
177
178impl Config {
179    /// Construct a new config where only the `cluster_url` is set by the user.
180    /// and everything else receives a default value.
181    ///
182    /// Most likely you want to use [`Config::infer`] to infer the config from
183    /// the environment.
184    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    /// Infer a Kubernetes client configuration.
204    ///
205    /// First, a user's kubeconfig is loaded from `KUBECONFIG` or
206    /// `~/.kube/config`. If that fails, an in-cluster config is loaded via
207    /// [`Config::incluster`]. If inference from both sources fails, then an
208    /// error is returned.
209    ///
210    /// [`Config::apply_debug_overrides`] is used to augment the loaded
211    /// configuration based on the environment.
212    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    /// Load an in-cluster Kubernetes client configuration using
232    /// [`Config::incluster_env`].
233    pub fn incluster() -> Result<Self, InClusterError> {
234        Self::incluster_env()
235    }
236
237    /// Load an in-cluster config using the `KUBERNETES_SERVICE_HOST` and
238    /// `KUBERNETES_SERVICE_PORT` environment variables.
239    ///
240    /// A service account's token must be available in
241    /// `/var/run/secrets/kubernetes.io/serviceaccount/`.
242    ///
243    /// This method matches the behavior of the official Kubernetes client
244    /// libraries and is the default for both TLS stacks.
245    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    /// Load an in-cluster config using the API server at
251    /// `https://kubernetes.default.svc`.
252    ///
253    /// A service account's token must be available in
254    /// `/var/run/secrets/kubernetes.io/serviceaccount/`.
255    ///
256    /// This behavior does not match that of the official Kubernetes clients,
257    /// but can be used as a consistent entrypoint in many clusters.
258    /// See <https://github.com/kube-rs/kube/issues/1003> for more info.
259    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    /// Create configuration from the default local config file
289    ///
290    /// This will respect the `$KUBECONFIG` evar, but otherwise default to `~/.kube/config`.
291    /// You can also customize what context/cluster/user you want to use here,
292    /// but it will default to the current-context.
293    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    /// Create configuration from a [`Kubeconfig`] struct
299    ///
300    /// This bypasses kube's normal config parsing to obtain custom functionality.
301    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    /// Override configuration based on environment variables
352    ///
353    /// This is only intended for use as a debugging aid, and the specific variables and their behaviour
354    /// should **not** be considered stable across releases.
355    ///
356    /// Currently, the following overrides are supported:
357    ///
358    /// - `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`
359    /// - `KUBE_RS_DEBUG_IMPERSONATE_GROUP`: A Kubernetes group to impersonate, multiple groups may be specified by separating them with commas
360    /// - `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`
361    pub fn apply_debug_overrides(&mut self) {
362        // Log these overrides loudly, to emphasize that this is only a debugging aid, and should not be relied upon in production
363        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    /// Client certificate and private key in PEM.
390    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
417// https://github.com/kube-rs/kube/issues/146#issuecomment-590924397
418const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
419const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(295);
420
421// Expose raw config structs
422pub 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"))] // want to ensure this works without client features
430    #[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}