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(#[source] serde_yaml::Error),
67
68    /// The structure of the parsed kubeconfig is invalid
69    #[error("the structure of the parsed kubeconfig is invalid: {0}")]
70    InvalidStructure(#[source] serde_yaml::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")]
86    LoadCertificateAuthority(#[source] LoadDataError),
87
88    /// Failed to load client certificate
89    #[error("failed to load client certificate")]
90    LoadClientCertificate(#[source] LoadDataError),
91
92    /// Failed to load client key
93    #[error("failed to load client key")]
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
101/// Errors from loading data from a base64 string or a file
102#[derive(Debug, Error)]
103pub enum LoadDataError {
104    /// Failed to decode base64 data
105    #[error("failed to decode base64 data: {0}")]
106    DecodeBase64(#[source] base64::DecodeError),
107
108    /// Failed to read file
109    #[error("failed to read file '{1:?}': {0}")]
110    ReadFile(#[source] std::io::Error, PathBuf),
111
112    /// No base64 data or file path was provided
113    #[error("no base64 data or file")]
114    NoBase64DataOrFile,
115}
116
117/// Configuration object for accessing a Kuernetes cluster
118///
119/// The configurable parameters for connecting like cluster URL, default namespace, root certificates, and timeouts.
120/// Normally created implicitly through [`Config::infer`] or [`Client::try_default`](crate::Client::try_default).
121///
122/// # Usage
123/// Construct a [`Config`] instance by using one of the many constructors.
124///
125/// Prefer [`Config::infer`] unless you have particular issues, and avoid manually managing
126/// the data in this struct unless you have particular needs. It exists to be consumed by the [`Client`][crate::Client].
127///
128/// If you are looking to parse the kubeconfig found in a user's home directory see [`Kubeconfig`].
129#[cfg_attr(docsrs, doc(cfg(feature = "config")))]
130#[derive(Debug, Clone)]
131pub struct Config {
132    /// The configured cluster url
133    pub cluster_url: http::Uri,
134    /// The configured default namespace
135    pub default_namespace: String,
136    /// The configured root certificate
137    pub root_cert: Option<Vec<Vec<u8>>>,
138    /// Set the timeout for connecting to the Kubernetes API.
139    ///
140    /// A value of `None` means no timeout
141    pub connect_timeout: Option<std::time::Duration>,
142    /// Set the timeout for the Kubernetes API response.
143    ///
144    /// A value of `None` means no timeout
145    pub read_timeout: Option<std::time::Duration>,
146    /// Set the timeout for the Kubernetes API request.
147    ///
148    /// A value of `None` means no timeout
149    pub write_timeout: Option<std::time::Duration>,
150    /// Whether to accept invalid certificates
151    pub accept_invalid_certs: bool,
152    /// Stores information to tell the cluster who you are.
153    pub auth_info: AuthInfo,
154    /// Whether to disable compression (would only have an effect when the `gzip` feature is enabled)
155    pub disable_compression: bool,
156    /// Optional proxy URL. Proxy support requires the `socks5` feature.
157    pub proxy_url: Option<http::Uri>,
158    /// If set, apiserver certificate will be validated to contain this string
159    ///
160    /// If not set, the `cluster_url` is used instead
161    pub tls_server_name: Option<String>,
162    /// Headers to pass with every request.
163    pub headers: Vec<(HeaderName, HeaderValue)>,
164}
165
166impl Config {
167    /// Construct a new config where only the `cluster_url` is set by the user.
168    /// and everything else receives a default value.
169    ///
170    /// Most likely you want to use [`Config::infer`] to infer the config from
171    /// the environment.
172    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    /// Infer a Kubernetes client configuration.
190    ///
191    /// First, a user's kubeconfig is loaded from `KUBECONFIG` or
192    /// `~/.kube/config`. If that fails, an in-cluster config is loaded via
193    /// [`Config::incluster`]. If inference from both sources fails, then an
194    /// error is returned.
195    ///
196    /// [`Config::apply_debug_overrides`] is used to augment the loaded
197    /// configuration based on the environment.
198    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    /// Load an in-cluster Kubernetes client configuration using
218    /// [`Config::incluster_env`].
219    pub fn incluster() -> Result<Self, InClusterError> {
220        Self::incluster_env()
221    }
222
223    /// Load an in-cluster config using the `KUBERNETES_SERVICE_HOST` and
224    /// `KUBERNETES_SERVICE_PORT` environment variables.
225    ///
226    /// A service account's token must be available in
227    /// `/var/run/secrets/kubernetes.io/serviceaccount/`.
228    ///
229    /// This method matches the behavior of the official Kubernetes client
230    /// libraries and is the default for both TLS stacks.
231    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    /// Load an in-cluster config using the API server at
237    /// `https://kubernetes.default.svc`.
238    ///
239    /// A service account's token must be available in
240    /// `/var/run/secrets/kubernetes.io/serviceaccount/`.
241    ///
242    /// This behavior does not match that of the official Kubernetes clients,
243    /// but can be used as a consistent entrypoint in many clusters.
244    /// See <https://github.com/kube-rs/kube/issues/1003> for more info.
245    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    /// Create configuration from the default local config file
273    ///
274    /// This will respect the `$KUBECONFIG` evar, but otherwise default to `~/.kube/config`.
275    /// You can also customize what context/cluster/user you want to use here,
276    /// but it will default to the current-context.
277    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    /// Create configuration from a [`Kubeconfig`] struct
283    ///
284    /// This bypasses kube's normal config parsing to obtain custom functionality.
285    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    /// Override configuration based on environment variables
334    ///
335    /// This is only intended for use as a debugging aid, and the specific variables and their behaviour
336    /// should **not** be considered stable across releases.
337    ///
338    /// Currently, the following overrides are supported:
339    ///
340    /// - `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`
341    /// - `KUBE_RS_DEBUG_IMPERSONATE_GROUP`: A Kubernetes group to impersonate, multiple groups may be specified by separating them with commas
342    /// - `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`
343    pub fn apply_debug_overrides(&mut self) {
344        // Log these overrides loudly, to emphasize that this is only a debugging aid, and should not be relied upon in production
345        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    /// Client certificate and private key in PEM.
372    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
390// https://github.com/kube-rs/kube/issues/146#issuecomment-590924397
391const 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
395// Expose raw config structs
396pub 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"))] // want to ensure this works without client features
404    #[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}