Skip to main content

kube_client/config/
file_config.rs

1use std::{
2    collections::HashMap,
3    fs, io,
4    path::{Path, PathBuf},
5};
6
7use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::{KubeconfigError, LoadDataError};
11
12/// [`CLUSTER_EXTENSION_KEY`] is reserved in the cluster extensions list for exec plugin config.
13const CLUSTER_EXTENSION_KEY: &str = "client.authentication.k8s.io/exec";
14
15/// [`Kubeconfig`] represents information on how to connect to a remote Kubernetes cluster
16///
17/// Stored in `~/.kube/config` by default, but can be distributed across multiple paths in passed through `KUBECONFIG`.
18/// An analogue of the [config type from client-go](https://github.com/kubernetes/client-go/blob/7697067af71046b18e03dbda04e01a5bb17f9809/tools/clientcmd/api/types.go).
19///
20/// This type (and its children) are exposed primarily for convenience.
21///
22/// [`Config`][crate::Config] is the __intended__ developer interface to help create a [`Client`][crate::Client],
23/// and this will handle the difference between in-cluster deployment and local development.
24#[derive(Clone, Debug, Serialize, Deserialize, Default)]
25#[cfg_attr(test, derive(PartialEq))]
26pub struct Kubeconfig {
27    /// General information to be use for cli interactions
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub preferences: Option<Preferences>,
30    /// Referencable names to cluster configs
31    #[serde(default, deserialize_with = "deserialize_null_as_default")]
32    pub clusters: Vec<NamedCluster>,
33    /// Referencable names to user configs
34    #[serde(rename = "users")]
35    #[serde(default, deserialize_with = "deserialize_null_as_default")]
36    pub auth_infos: Vec<NamedAuthInfo>,
37    /// Referencable names to context configs
38    #[serde(default, deserialize_with = "deserialize_null_as_default")]
39    pub contexts: Vec<NamedContext>,
40    /// The name of the context that you would like to use by default
41    #[serde(rename = "current-context")]
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub current_context: Option<String>,
44    /// Additional information for extenders so that reads and writes don't clobber unknown fields.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub extensions: Option<Vec<NamedExtension>>,
47
48    // legacy fields TODO: remove
49    /// Legacy field from TypeMeta
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub kind: Option<String>,
52    /// Legacy field from TypeMeta
53    #[serde(rename = "apiVersion")]
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub api_version: Option<String>,
56}
57
58/// Preferences stores extensions for cli.
59#[derive(Clone, Debug, Serialize, Deserialize)]
60#[cfg_attr(test, derive(PartialEq, Eq))]
61pub struct Preferences {
62    /// Enable colors
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub colors: Option<bool>,
65    /// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub extensions: Option<Vec<NamedExtension>>,
68}
69
70/// NamedExtention associates name with extension.
71#[derive(Clone, Debug, Serialize, Deserialize)]
72#[cfg_attr(test, derive(PartialEq, Eq))]
73pub struct NamedExtension {
74    /// Name of extension
75    pub name: String,
76    /// Additional information for extenders so that reads and writes don't clobber unknown fields
77    pub extension: serde_json::Value,
78}
79
80/// NamedCluster associates name with cluster.
81#[derive(Clone, Debug, Serialize, Deserialize, Default)]
82#[cfg_attr(test, derive(PartialEq, Eq))]
83pub struct NamedCluster {
84    /// Name of cluster
85    pub name: String,
86    /// Information about how to communicate with a kubernetes cluster
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub cluster: Option<Cluster>,
89}
90
91/// Cluster stores information to connect Kubernetes cluster.
92#[derive(Clone, Debug, Serialize, Deserialize, Default)]
93#[cfg_attr(test, derive(PartialEq, Eq))]
94pub struct Cluster {
95    /// The address of the kubernetes cluster (https://hostname:port).
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub server: Option<String>,
98    /// Skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
99    #[serde(rename = "insecure-skip-tls-verify")]
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub insecure_skip_tls_verify: Option<bool>,
102    /// The path to a cert file for the certificate authority.
103    #[serde(rename = "certificate-authority")]
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub certificate_authority: Option<String>,
106    /// PEM-encoded certificate authority certificates. Overrides `certificate_authority`
107    #[serde(rename = "certificate-authority-data")]
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub certificate_authority_data: Option<String>,
110    /// URL to the proxy to be used for all requests.
111    #[serde(rename = "proxy-url")]
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub proxy_url: Option<String>,
114    /// Compression is enabled by default with the `gzip` feature.
115    /// `disable_compression` allows client to opt-out of response compression for all requests to the server.
116    /// This is useful to speed up requests (specifically lists) when client-server network bandwidth is ample,
117    /// by saving time on compression (server-side) and decompression (client-side):
118    /// https://github.com/kubernetes/kubernetes/issues/112296
119    #[serde(rename = "disable-compression")]
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub disable_compression: Option<bool>,
122    /// Name used to check server certificate.
123    ///
124    /// If `tls_server_name` is `None`, the hostname used to contact the server is used.
125    #[serde(rename = "tls-server-name")]
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub tls_server_name: Option<String>,
128    /// Additional information for extenders so that reads and writes don't clobber unknown fields
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub extensions: Option<Vec<NamedExtension>>,
131}
132
133/// NamedAuthInfo associates name with authentication.
134#[derive(Clone, Debug, Serialize, Deserialize, Default)]
135#[cfg_attr(test, derive(PartialEq))]
136pub struct NamedAuthInfo {
137    /// Name of the user
138    pub name: String,
139    /// Information that describes identity of the user
140    #[serde(rename = "user")]
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub auth_info: Option<AuthInfo>,
143}
144
145fn serialize_secretstring<S>(pw: &Option<SecretString>, serializer: S) -> Result<S::Ok, S::Error>
146where
147    S: Serializer,
148{
149    match pw {
150        Some(secret) => serializer.serialize_str(secret.expose_secret()),
151        None => serializer.serialize_none(),
152    }
153}
154
155fn deserialize_secretstring<'de, D>(deserializer: D) -> Result<Option<SecretString>, D::Error>
156where
157    D: Deserializer<'de>,
158{
159    match Option::<String>::deserialize(deserializer) {
160        Ok(Some(secret)) => Ok(Some(SecretString::new(secret.into()))),
161        Ok(None) => Ok(None),
162        Err(e) => Err(e),
163    }
164}
165
166fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
167where
168    T: Default + Deserialize<'de>,
169    D: Deserializer<'de>,
170{
171    let opt = Option::deserialize(deserializer)?;
172    Ok(opt.unwrap_or_default())
173}
174
175/// AuthInfo stores information to tell cluster who you are.
176#[derive(Clone, Debug, Serialize, Deserialize, Default)]
177pub struct AuthInfo {
178    /// The username for basic authentication to the kubernetes cluster.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub username: Option<String>,
181    /// The password for basic authentication to the kubernetes cluster.
182    #[serde(skip_serializing_if = "Option::is_none", default)]
183    #[serde(
184        serialize_with = "serialize_secretstring",
185        deserialize_with = "deserialize_secretstring"
186    )]
187    pub password: Option<SecretString>,
188
189    /// The bearer token for authentication to the kubernetes cluster.
190    #[serde(skip_serializing_if = "Option::is_none", default)]
191    #[serde(
192        serialize_with = "serialize_secretstring",
193        deserialize_with = "deserialize_secretstring"
194    )]
195    pub token: Option<SecretString>,
196    /// Pointer to a file that contains a bearer token (as described above). If both `token` and token_file` are present, `token` takes precedence.
197    #[serde(rename = "tokenFile")]
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub token_file: Option<String>,
200
201    /// Path to a client cert file for TLS.
202    #[serde(rename = "client-certificate")]
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub client_certificate: Option<String>,
205    /// PEM-encoded data from a client cert file for TLS. Overrides `client_certificate`
206    /// this key should be base64 encoded instead of the decode string data
207    #[serde(rename = "client-certificate-data")]
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub client_certificate_data: Option<String>,
210
211    /// Path to a client key file for TLS.
212    #[serde(rename = "client-key")]
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub client_key: Option<String>,
215    /// PEM-encoded data from a client key file for TLS. Overrides `client_key`
216    /// this key should be base64 encoded instead of the decode string data
217    #[serde(rename = "client-key-data")]
218    #[serde(skip_serializing_if = "Option::is_none", default)]
219    #[serde(
220        serialize_with = "serialize_secretstring",
221        deserialize_with = "deserialize_secretstring"
222    )]
223    pub client_key_data: Option<SecretString>,
224
225    /// The username to act-as.
226    #[serde(rename = "as")]
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub impersonate: Option<String>,
229    /// The groups to imperonate.
230    #[serde(rename = "as-groups")]
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub impersonate_groups: Option<Vec<String>>,
233
234    /// Specifies a custom authentication plugin for the kubernetes cluster.
235    #[serde(rename = "auth-provider")]
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub auth_provider: Option<AuthProviderConfig>,
238
239    /// Specifies a custom exec-based authentication plugin for the kubernetes cluster.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub exec: Option<ExecConfig>,
242}
243
244#[cfg(test)]
245impl PartialEq for AuthInfo {
246    fn eq(&self, other: &Self) -> bool {
247        serde_json::to_value(self).unwrap() == serde_json::to_value(other).unwrap()
248    }
249}
250
251/// AuthProviderConfig stores auth for specified cloud provider.
252#[derive(Clone, Debug, Serialize, Deserialize)]
253#[cfg_attr(test, derive(PartialEq, Eq))]
254pub struct AuthProviderConfig {
255    /// Name of the auth provider
256    pub name: String,
257    /// Auth provider configuration
258    #[serde(default)]
259    pub config: HashMap<String, String>,
260}
261
262/// ExecConfig stores credential-plugin configuration.
263#[derive(Clone, Debug, Serialize, Deserialize)]
264#[cfg_attr(test, derive(PartialEq, Eq))]
265pub struct ExecConfig {
266    /// Preferred input version of the ExecInfo.
267    ///
268    /// The returned ExecCredentials MUST use the same encoding version as the input.
269    #[serde(rename = "apiVersion")]
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub api_version: Option<String>,
272    /// Command to execute.
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub command: Option<String>,
275    /// Arguments to pass to the command when executing it.
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub args: Option<Vec<String>>,
278    /// Env defines additional environment variables to expose to the process.
279    ///
280    /// TODO: These are unioned with the host's environment, as well as variables client-go uses to pass argument to the plugin.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub env: Option<Vec<HashMap<String, String>>>,
283    /// Specifies which environment variables the host should avoid passing to the auth plugin.
284    ///
285    /// This does currently not exist upstream and cannot be specified on disk.
286    /// It has been suggested in client-go via <https://github.com/kubernetes/client-go/issues/1177>
287    #[serde(skip)]
288    pub drop_env: Option<Vec<String>>,
289
290    /// Interative mode of the auth plugins
291    #[serde(rename = "interactiveMode")]
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub interactive_mode: Option<ExecInteractiveMode>,
294
295    /// ProvideClusterInfo determines whether or not to provide cluster information,
296    /// which could potentially contain very large CA data, to this exec plugin as a
297    /// part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set
298    /// to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for
299    /// reading this environment variable.
300    #[serde(default, rename = "provideClusterInfo")]
301    pub provide_cluster_info: bool,
302
303    /// Cluster information to pass to the plugin.
304    /// Should be used only when `provide_cluster_info` is True.
305    #[serde(skip)]
306    pub cluster: Option<ExecAuthCluster>,
307}
308
309/// ExecInteractiveMode define the interactity of the child process
310#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
311#[cfg_attr(test, derive(Eq))]
312pub enum ExecInteractiveMode {
313    /// Never get interactive
314    Never,
315    /// If available et interactive
316    IfAvailable,
317    /// Alwayes get interactive
318    Always,
319}
320
321/// NamedContext associates name with context.
322#[derive(Clone, Debug, Serialize, Deserialize, Default)]
323#[cfg_attr(test, derive(PartialEq, Eq))]
324pub struct NamedContext {
325    /// Name of the context
326    pub name: String,
327    /// Associations for the context
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub context: Option<Context>,
330}
331
332/// Context stores tuple of cluster and user information.
333#[derive(Clone, Debug, Serialize, Deserialize, Default)]
334#[cfg_attr(test, derive(PartialEq, Eq))]
335pub struct Context {
336    /// Name of the cluster for this context
337    pub cluster: String,
338    /// Name of the `AuthInfo` for this context
339    pub user: Option<String>,
340    /// The default namespace to use on unspecified requests
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub namespace: Option<String>,
343    /// Additional information for extenders so that reads and writes don't clobber unknown fields
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub extensions: Option<Vec<NamedExtension>>,
346}
347
348const KUBECONFIG: &str = "KUBECONFIG";
349
350/// Some helpers on the raw Config object are exposed for people needing to parse it
351impl Kubeconfig {
352    /// Read a Config from an arbitrary location
353    pub fn read_from<P: AsRef<Path>>(path: P) -> Result<Kubeconfig, KubeconfigError> {
354        let data =
355            read_path(&path).map_err(|source| KubeconfigError::ReadConfig(source, path.as_ref().into()))?;
356
357        // Remap all files we read to absolute paths.
358        let mut merged_docs = None;
359        for mut config in kubeconfig_from_yaml(&data)? {
360            if let Some(dir) = path.as_ref().parent() {
361                for named in config.clusters.iter_mut() {
362                    if let Some(cluster) = &mut named.cluster
363                        && let Some(path) = &cluster.certificate_authority
364                        && let Some(abs_path) = to_absolute(dir, path)
365                    {
366                        cluster.certificate_authority = Some(abs_path);
367                    }
368                }
369                for named in config.auth_infos.iter_mut() {
370                    if let Some(auth_info) = &mut named.auth_info {
371                        if let Some(path) = &auth_info.client_certificate
372                            && let Some(abs_path) = to_absolute(dir, path)
373                        {
374                            auth_info.client_certificate = Some(abs_path);
375                        }
376                        if let Some(path) = &auth_info.client_key
377                            && let Some(abs_path) = to_absolute(dir, path)
378                        {
379                            auth_info.client_key = Some(abs_path);
380                        }
381                        if let Some(path) = &auth_info.token_file
382                            && let Some(abs_path) = to_absolute(dir, path)
383                        {
384                            auth_info.token_file = Some(abs_path);
385                        }
386                    }
387                }
388            }
389            if let Some(c) = merged_docs {
390                merged_docs = Some(Kubeconfig::merge(c, config)?);
391            } else {
392                merged_docs = Some(config);
393            }
394        }
395        // Empty file defaults to an empty Kubeconfig
396        Ok(merged_docs.unwrap_or_default())
397    }
398
399    /// Read a Config from an arbitrary YAML string
400    ///
401    /// This is preferable to using serde_yaml::from_str() because it will correctly
402    /// parse multi-document YAML text and merge them into a single `Kubeconfig`
403    pub fn from_yaml(text: &str) -> Result<Kubeconfig, KubeconfigError> {
404        kubeconfig_from_yaml(text)?
405            .into_iter()
406            .try_fold(Kubeconfig::default(), Kubeconfig::merge)
407    }
408
409    /// Read a Config from `KUBECONFIG` or the the default location.
410    pub fn read() -> Result<Kubeconfig, KubeconfigError> {
411        match Self::from_env()? {
412            Some(config) => Ok(config),
413            None => Self::read_from(default_kube_path().ok_or(KubeconfigError::FindPath)?),
414        }
415    }
416
417    /// Create `Kubeconfig` from `KUBECONFIG` environment variable.
418    /// Supports list of files to be merged.
419    ///
420    /// # Panics
421    ///
422    /// Panics if `KUBECONFIG` value contains the NUL character.
423    pub fn from_env() -> Result<Option<Self>, KubeconfigError> {
424        match std::env::var_os(KUBECONFIG) {
425            Some(value) => {
426                let paths = std::env::split_paths(&value)
427                    .filter(|p| !p.as_os_str().is_empty())
428                    .collect::<Vec<_>>();
429                if paths.is_empty() {
430                    return Ok(None);
431                }
432
433                let merged = paths.iter().try_fold(Kubeconfig::default(), |m, p| {
434                    Kubeconfig::read_from(p).and_then(|c| m.merge(c))
435                })?;
436                Ok(Some(merged))
437            }
438
439            None => Ok(None),
440        }
441    }
442
443    /// Merge kubeconfig file according to the rules described in
444    /// <https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files>
445    ///
446    /// > Merge the files listed in the `KUBECONFIG` environment variable according to these rules:
447    /// >
448    /// > - Ignore empty filenames.
449    /// > - Produce errors for files with content that cannot be deserialized.
450    /// > - The first file to set a particular value or map key wins.
451    /// > - Never change the value or map key.
452    /// >   Example: Preserve the context of the first file to set `current-context`.
453    /// >   Example: If two files specify a `red-user`, use only values from the first file's `red-user`.
454    /// >            Even if the second file has non-conflicting entries under `red-user`, discard them.
455    pub fn merge(mut self, next: Kubeconfig) -> Result<Self, KubeconfigError> {
456        if self.kind.is_some() && next.kind.is_some() && self.kind != next.kind {
457            return Err(KubeconfigError::KindMismatch);
458        }
459        if self.api_version.is_some() && next.api_version.is_some() && self.api_version != next.api_version {
460            return Err(KubeconfigError::ApiVersionMismatch);
461        }
462
463        self.kind = self.kind.or(next.kind);
464        self.api_version = self.api_version.or(next.api_version);
465        self.preferences = self.preferences.or(next.preferences);
466        append_new_named(&mut self.clusters, next.clusters, |x| &x.name);
467        append_new_named(&mut self.auth_infos, next.auth_infos, |x| &x.name);
468        append_new_named(&mut self.contexts, next.contexts, |x| &x.name);
469        self.current_context = self.current_context.or(next.current_context);
470        self.extensions = self.extensions.or(next.extensions);
471        Ok(self)
472    }
473}
474
475fn kubeconfig_from_yaml(text: &str) -> Result<Vec<Kubeconfig>, KubeconfigError> {
476    let mut documents = vec![];
477    for doc in serde_yaml::Deserializer::from_str(text) {
478        let value = serde_yaml::Value::deserialize(doc).map_err(KubeconfigError::Parse)?;
479        let kubeconfig = serde_yaml::from_value(value).map_err(KubeconfigError::InvalidStructure)?;
480        documents.push(kubeconfig);
481    }
482    Ok(documents)
483}
484
485#[allow(clippy::redundant_closure)]
486fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
487where
488    F: Fn(&T) -> &String,
489{
490    use std::collections::HashSet;
491    base.extend({
492        let existing = base.iter().map(|x| f(x)).collect::<HashSet<_>>();
493        next.into_iter()
494            .filter(|x| !existing.contains(f(x)))
495            .collect::<Vec<_>>()
496    });
497}
498
499fn read_path<P: AsRef<Path>>(path: P) -> io::Result<String> {
500    let bytes = fs::read(&path)?;
501    match bytes.as_slice() {
502        [0xFF, 0xFE, ..] => {
503            let utf16_data: Vec<u16> = bytes[2..]
504                .chunks(2)
505                .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
506                .collect();
507            String::from_utf16(&utf16_data)
508                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 LE"))
509        }
510        [0xFE, 0xFF, ..] => {
511            let utf16_data: Vec<u16> = bytes[2..]
512                .chunks(2)
513                .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
514                .collect();
515            String::from_utf16(&utf16_data)
516                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 BE"))
517        }
518        [0xEF, 0xBB, 0xBF, ..] => String::from_utf8(bytes[3..].to_vec())
519            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 BOM")),
520        _ => {
521            String::from_utf8(bytes).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8"))
522        }
523    }
524}
525
526fn to_absolute(dir: &Path, file: &str) -> Option<String> {
527    let path = Path::new(&file);
528    if path.is_relative() {
529        dir.join(path).to_str().map(str::to_owned)
530    } else {
531        None
532    }
533}
534
535impl Cluster {
536    pub(crate) fn load_certificate_authority(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
537        if self.certificate_authority.is_none() && self.certificate_authority_data.is_none() {
538            return Ok(None);
539        }
540
541        let ca = load_from_base64_or_file(
542            &self.certificate_authority_data.as_deref(),
543            &self.certificate_authority,
544        )
545        .map_err(KubeconfigError::LoadCertificateAuthority)?;
546        Ok(Some(ca))
547    }
548}
549
550impl AuthInfo {
551    pub(crate) fn identity_pem(&self) -> Result<Vec<u8>, KubeconfigError> {
552        let client_cert = &self.load_client_certificate()?;
553        let client_key = &self.load_client_key()?;
554        let mut buffer = client_key.clone();
555        buffer.extend_from_slice(client_cert);
556        Ok(buffer)
557    }
558
559    pub(crate) fn load_client_certificate(&self) -> Result<Vec<u8>, KubeconfigError> {
560        // TODO Shouldn't error when `self.client_certificate_data.is_none() && self.client_certificate.is_none()`
561
562        load_from_base64_or_file(&self.client_certificate_data.as_deref(), &self.client_certificate)
563            .map_err(KubeconfigError::LoadClientCertificate)
564    }
565
566    pub(crate) fn load_client_key(&self) -> Result<Vec<u8>, KubeconfigError> {
567        // TODO Shouldn't error when `self.client_key_data.is_none() && self.client_key.is_none()`
568
569        load_from_base64_or_file(
570            &self.client_key_data.as_ref().map(|secret| secret.expose_secret()),
571            &self.client_key,
572        )
573        .map_err(KubeconfigError::LoadClientKey)
574    }
575}
576
577/// Connection information for auth plugins that have `provideClusterInfo` enabled.
578///
579/// This is a copy of [`kube::config::Cluster`] with certificate_authority passed as bytes without the path.
580/// Taken from [clientauthentication/types.go#Cluster](https://github.com/kubernetes/client-go/blob/477cb782cf024bc70b7239f0dca91e5774811950/pkg/apis/clientauthentication/types.go#L73-L129)
581#[derive(Clone, Debug, Serialize, Deserialize, Default)]
582#[serde(rename_all = "kebab-case")]
583#[cfg_attr(test, derive(PartialEq, Eq))]
584pub struct ExecAuthCluster {
585    /// The address of the kubernetes cluster (https://hostname:port).
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub server: Option<String>,
588    /// Skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub insecure_skip_tls_verify: Option<bool>,
591    /// PEM-encoded certificate authority certificates. Overrides `certificate_authority`
592    #[serde(default, skip_serializing_if = "Option::is_none")]
593    #[serde(with = "base64serde")]
594    pub certificate_authority_data: Option<Vec<u8>>,
595    /// URL to the proxy to be used for all requests.
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub proxy_url: Option<String>,
598    /// Name used to check server certificate.
599    ///
600    /// If `tls_server_name` is `None`, the hostname used to contact the server is used.
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub tls_server_name: Option<String>,
603    /// This can be anything
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub config: Option<serde_json::Value>,
606}
607
608impl TryFrom<&Cluster> for ExecAuthCluster {
609    type Error = KubeconfigError;
610
611    fn try_from(cluster: &crate::config::Cluster) -> Result<Self, KubeconfigError> {
612        let certificate_authority_data = cluster.load_certificate_authority()?;
613        Ok(Self {
614            server: cluster.server.clone(),
615            insecure_skip_tls_verify: cluster.insecure_skip_tls_verify,
616            certificate_authority_data,
617            proxy_url: cluster.proxy_url.clone(),
618            tls_server_name: cluster.tls_server_name.clone(),
619            config: cluster.extensions.as_ref().and_then(|extensions| {
620                extensions
621                    .iter()
622                    .find(|extension| extension.name == CLUSTER_EXTENSION_KEY)
623                    .map(|extension| extension.extension.clone())
624            }),
625        })
626    }
627}
628
629fn load_from_base64_or_file<P: AsRef<Path>>(
630    value: &Option<&str>,
631    file: &Option<P>,
632) -> Result<Vec<u8>, LoadDataError> {
633    let data = value
634        .map(load_from_base64)
635        .or_else(|| file.as_ref().map(load_from_file))
636        .unwrap_or_else(|| Err(LoadDataError::NoBase64DataOrFile))?;
637    Ok(ensure_trailing_newline(data))
638}
639
640fn load_from_base64(value: &str) -> Result<Vec<u8>, LoadDataError> {
641    use base64::Engine;
642    base64::engine::general_purpose::STANDARD
643        .decode(value)
644        .map_err(LoadDataError::DecodeBase64)
645}
646
647fn load_from_file<P: AsRef<Path>>(file: &P) -> Result<Vec<u8>, LoadDataError> {
648    fs::read(file).map_err(|source| LoadDataError::ReadFile(source, file.as_ref().into()))
649}
650
651// Ensure there is a trailing newline in the blob
652// Don't bother if the blob is empty
653fn ensure_trailing_newline(mut data: Vec<u8>) -> Vec<u8> {
654    if data.last().map(|end| *end != b'\n').unwrap_or(false) {
655        data.push(b'\n');
656    }
657    data
658}
659
660/// Returns kubeconfig path from `$HOME/.kube/config`.
661fn default_kube_path() -> Option<PathBuf> {
662    // Before Rust 1.85.0, `home_dir` would return wrong results on Windows, usage of the crate
663    // `home` was encouraged (and is what kube-rs did).
664    // Rust 1.85.0 fixed the problem (https://doc.rust-lang.org/1.85.0/std/env/fn.home_dir.html),
665    // Rust 1.87.0 removed the function deprecation.
666    // As the MSRV was bumped to 1.85.0 we are safe to use the fixed std function.
667    // Note: We intentionally use `allow` over `expect` to support compilation on Rust >= 1.87.0
668    // Note: This can be removed once the MSRV is bumped to >= 1.87.0
669    #[allow(deprecated)]
670    std::env::home_dir().map(|h| h.join(".kube").join("config"))
671}
672
673mod base64serde {
674    use base64::Engine;
675    use serde::{Deserialize, Deserializer, Serialize, Serializer};
676
677    pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
678        match v {
679            Some(v) => {
680                let encoded = base64::engine::general_purpose::STANDARD.encode(v);
681                String::serialize(&encoded, s)
682            }
683            None => <Option<String>>::serialize(&None, s),
684        }
685    }
686
687    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
688        let data = <Option<String>>::deserialize(d)?;
689        match data {
690            Some(data) => Ok(Some(
691                base64::engine::general_purpose::STANDARD
692                    .decode(data.as_bytes())
693                    .map_err(serde::de::Error::custom)?,
694            )),
695            None => Ok(None),
696        }
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use crate::config::file_loader::ConfigLoader;
703
704    use super::*;
705    use serde_json::{Value, json};
706
707    #[test]
708    fn kubeconfig_merge() {
709        let kubeconfig1 = Kubeconfig {
710            current_context: Some("default".into()),
711            auth_infos: vec![NamedAuthInfo {
712                name: "red-user".into(),
713                auth_info: Some(AuthInfo {
714                    token: Some(SecretString::new("first-token".into())),
715                    ..Default::default()
716                }),
717            }],
718            ..Default::default()
719        };
720        let kubeconfig2 = Kubeconfig {
721            current_context: Some("dev".into()),
722            auth_infos: vec![
723                NamedAuthInfo {
724                    name: "red-user".into(),
725                    auth_info: Some(AuthInfo {
726                        token: Some(SecretString::new("second-token".into())),
727                        username: Some("red-user".into()),
728                        ..Default::default()
729                    }),
730                },
731                NamedAuthInfo {
732                    name: "green-user".into(),
733                    auth_info: Some(AuthInfo {
734                        token: Some(SecretString::new("new-token".into())),
735                        ..Default::default()
736                    }),
737                },
738            ],
739            ..Default::default()
740        };
741
742        let merged = kubeconfig1.merge(kubeconfig2).unwrap();
743        // Preserves first `current_context`
744        assert_eq!(merged.current_context, Some("default".into()));
745        // Auth info with the same name does not overwrite
746        assert_eq!(merged.auth_infos[0].name, "red-user");
747        assert_eq!(
748            merged.auth_infos[0]
749                .auth_info
750                .as_ref()
751                .unwrap()
752                .token
753                .as_ref()
754                .map(|t| t.expose_secret()),
755            Some("first-token")
756        );
757        // Even if it's not conflicting
758        assert_eq!(merged.auth_infos[0].auth_info.as_ref().unwrap().username, None);
759        // New named auth info is appended
760        assert_eq!(merged.auth_infos[1].name, "green-user");
761    }
762
763    #[test]
764    fn kubeconfig_deserialize() {
765        let config_yaml = "apiVersion: v1
766clusters:
767- cluster:
768    certificate-authority-data: LS0t<SNIP>LS0tLQo=
769    server: https://ABCDEF0123456789.gr7.us-west-2.eks.amazonaws.com
770  name: eks
771- cluster:
772    certificate-authority: /home/kevin/.minikube/ca.crt
773    extensions:
774    - extension:
775        last-update: Thu, 18 Feb 2021 16:59:26 PST
776        provider: minikube.sigs.k8s.io
777        version: v1.17.1
778      name: cluster_info
779    server: https://192.168.49.2:8443
780  name: minikube
781contexts:
782- context:
783    cluster: minikube
784    extensions:
785    - extension:
786        last-update: Thu, 18 Feb 2021 16:59:26 PST
787        provider: minikube.sigs.k8s.io
788        version: v1.17.1
789      name: context_info
790    namespace: default
791    user: minikube
792  name: minikube
793- context:
794    cluster: arn:aws:eks:us-west-2:012345678912:cluster/eks
795    user: arn:aws:eks:us-west-2:012345678912:cluster/eks
796  name: eks
797current-context: minikube
798kind: Config
799preferences: {}
800users:
801- name: arn:aws:eks:us-west-2:012345678912:cluster/eks
802  user:
803    exec:
804      apiVersion: client.authentication.k8s.io/v1alpha1
805      args:
806      - --region
807      - us-west-2
808      - eks
809      - get-token
810      - --cluster-name
811      - eks
812      command: aws
813      env: null
814      provideClusterInfo: false
815- name: minikube
816  user:
817    client-certificate: /home/kevin/.minikube/profiles/minikube/client.crt
818    client-key: /home/kevin/.minikube/profiles/minikube/client.key";
819
820        let config = Kubeconfig::from_yaml(config_yaml).unwrap();
821
822        assert_eq!(config.clusters[0].name, "eks");
823        assert_eq!(config.clusters[1].name, "minikube");
824
825        let cluster1 = config.clusters[1].cluster.as_ref().unwrap();
826        assert_eq!(
827            cluster1.extensions.as_ref().unwrap()[0].extension.get("provider"),
828            Some(&Value::String("minikube.sigs.k8s.io".to_owned()))
829        );
830    }
831
832    #[test]
833    fn kubeconfig_multi_document_merge() -> Result<(), KubeconfigError> {
834        let config_yaml = r#"---
835apiVersion: v1
836clusters:
837- cluster:
838    certificate-authority-data: aGVsbG8K
839    server: https://0.0.0.0:6443
840  name: k3d-promstack
841contexts:
842- context:
843    cluster: k3d-promstack
844    user: admin@k3d-promstack
845  name: k3d-promstack
846current-context: k3d-promstack
847kind: Config
848preferences: {}
849users:
850- name: admin@k3d-promstack
851  user:
852    client-certificate-data: aGVsbG8K
853    client-key-data: aGVsbG8K
854---
855apiVersion: v1
856clusters:
857- cluster:
858    certificate-authority-data: aGVsbG8K
859    server: https://0.0.0.0:6443
860  name: k3d-k3s-default
861contexts:
862- context:
863    cluster: k3d-k3s-default
864    user: admin@k3d-k3s-default
865  name: k3d-k3s-default
866current-context: k3d-k3s-default
867kind: Config
868preferences: {}
869users:
870- name: admin@k3d-k3s-default
871  user:
872    client-certificate-data: aGVsbG8K
873    client-key-data: aGVsbG8K
874"#;
875        let cfg = Kubeconfig::from_yaml(config_yaml)?;
876
877        // Ensure we have data from both documents:
878        assert_eq!(cfg.clusters[0].name, "k3d-promstack");
879        assert_eq!(cfg.clusters[1].name, "k3d-k3s-default");
880
881        Ok(())
882    }
883
884    #[test]
885    fn kubeconfig_split_sections_merge() -> Result<(), KubeconfigError> {
886        let config1 = r#"
887apiVersion: v1
888clusters:
889- cluster:
890    certificate-authority-data: aGVsbG8K
891    server: https://0.0.0.0:6443
892  name: k3d-promstack
893contexts:
894- context:
895    cluster: k3d-promstack
896    user: admin@k3d-promstack
897  name: k3d-promstack
898current-context: k3d-promstack
899kind: Config
900preferences: {}
901"#;
902
903        let config2 = r#"
904users:
905- name: admin@k3d-k3s-default
906  user:
907    client-certificate-data: aGVsbG8K
908    client-key-data: aGVsbG8K
909"#;
910
911        let kubeconfig1 = Kubeconfig::from_yaml(config1)?;
912        let kubeconfig2 = Kubeconfig::from_yaml(config2)?;
913        let merged = kubeconfig1.merge(kubeconfig2).unwrap();
914
915        // Ensure we have data from both files:
916        assert_eq!(merged.clusters[0].name, "k3d-promstack");
917        assert_eq!(merged.contexts[0].name, "k3d-promstack");
918        assert_eq!(merged.auth_infos[0].name, "admin@k3d-k3s-default");
919
920        Ok(())
921    }
922
923    #[test]
924    fn kubeconfig_from_empty_string() {
925        let cfg = Kubeconfig::from_yaml("").unwrap();
926
927        assert_eq!(cfg, Kubeconfig::default());
928    }
929
930    #[test]
931    fn authinfo_deserialize_null_secret() {
932        let authinfo_yaml = r#"
933username: user
934password: 
935"#;
936        let authinfo: AuthInfo = serde_yaml::from_str(authinfo_yaml).unwrap();
937        assert_eq!(authinfo.username, Some("user".to_string()));
938        assert!(authinfo.password.is_none());
939    }
940
941    #[test]
942    fn authinfo_debug_does_not_output_password() {
943        let authinfo_yaml = r#"
944username: user
945password: kube_rs
946"#;
947        let authinfo: AuthInfo = serde_yaml::from_str(authinfo_yaml).unwrap();
948        let authinfo_debug_output = format!("{authinfo:?}");
949        let expected_output = "AuthInfo { \
950        username: Some(\"user\"), \
951        password: Some(SecretBox<str>([REDACTED])), \
952        token: None, token_file: None, client_certificate: None, \
953        client_certificate_data: None, client_key: None, \
954        client_key_data: None, impersonate: None, \
955        impersonate_groups: None, \
956        auth_provider: None, \
957        exec: None \
958        }";
959
960        assert_eq!(authinfo_debug_output, expected_output)
961    }
962
963    #[tokio::test]
964    async fn authinfo_exec_provide_cluster_info() {
965        let config = r#"
966apiVersion: v1
967clusters:
968- cluster:
969    server: https://localhost:8080
970    extensions:
971    - name: client.authentication.k8s.io/exec
972      extension:
973        audience: foo
974        other: bar
975  name: foo-cluster
976contexts:
977- context:
978    cluster: foo-cluster
979    user: foo-user
980    namespace: bar
981  name: foo-context
982current-context: foo-context
983kind: Config
984users:
985- name: foo-user
986  user:
987    exec:
988      apiVersion: client.authentication.k8s.io/v1alpha1
989      args:
990      - arg-1
991      - arg-2
992      command: foo-command
993      provideClusterInfo: true
994"#;
995        let kube_config = Kubeconfig::from_yaml(config).unwrap();
996        let config_loader = ConfigLoader::load(kube_config, None, None, None).await.unwrap();
997        let auth_info = config_loader.user;
998        let exec = auth_info.exec.unwrap();
999        assert!(exec.provide_cluster_info);
1000        let cluster = exec.cluster.unwrap();
1001        assert_eq!(
1002            cluster.config.unwrap(),
1003            json!({"audience": "foo", "other": "bar"})
1004        );
1005    }
1006
1007    #[tokio::test]
1008    async fn parse_kubeconfig_encodings() {
1009        let files = vec![
1010            "kubeconfig_utf8.yaml",
1011            "kubeconfig_utf16le.yaml",
1012            "kubeconfig_utf16be.yaml",
1013        ];
1014
1015        for file_name in files {
1016            let path = PathBuf::from(format!(
1017                "{}/src/config/test_data/{}",
1018                env!("CARGO_MANIFEST_DIR"),
1019                file_name
1020            ));
1021            let cfg = Kubeconfig::read_from(path).unwrap();
1022            assert_eq!(cfg.clusters[0].name, "k3d-promstack");
1023            assert_eq!(cfg.contexts[0].name, "k3d-promstack");
1024            assert_eq!(cfg.auth_infos[0].name, "admin@k3d-k3s-default");
1025        }
1026    }
1027}