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    /// Referenceable names to cluster configs
31    #[serde(default, deserialize_with = "deserialize_null_as_default")]
32    pub clusters: Vec<NamedCluster>,
33    /// Referenceable names to user configs
34    #[serde(rename = "users")]
35    #[serde(default, deserialize_with = "deserialize_null_as_default")]
36    pub auth_infos: Vec<NamedAuthInfo>,
37    /// Referenceable 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/// NamedExtension 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    /// Interactive 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
485fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
486where
487    F: Fn(&T) -> &String,
488{
489    use std::collections::HashSet;
490    base.extend({
491        let existing = base.iter().map(&f).collect::<HashSet<_>>();
492        next.into_iter()
493            .filter(|x| !existing.contains(f(x)))
494            .collect::<Vec<_>>()
495    });
496}
497
498fn read_path<P: AsRef<Path>>(path: P) -> io::Result<String> {
499    let bytes = fs::read(&path)?;
500    match bytes.as_slice() {
501        [0xFF, 0xFE, ..] => {
502            let utf16_data: Vec<u16> = bytes[2..]
503                .chunks(2)
504                .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
505                .collect();
506            String::from_utf16(&utf16_data)
507                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 LE"))
508        }
509        [0xFE, 0xFF, ..] => {
510            let utf16_data: Vec<u16> = bytes[2..]
511                .chunks(2)
512                .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
513                .collect();
514            String::from_utf16(&utf16_data)
515                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 BE"))
516        }
517        [0xEF, 0xBB, 0xBF, ..] => String::from_utf8(bytes[3..].to_vec())
518            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 BOM")),
519        _ => {
520            String::from_utf8(bytes).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8"))
521        }
522    }
523}
524
525fn to_absolute(dir: &Path, file: &str) -> Option<String> {
526    let path = Path::new(&file);
527    if path.is_relative() {
528        dir.join(path).to_str().map(str::to_owned)
529    } else {
530        None
531    }
532}
533
534impl Cluster {
535    pub(crate) fn load_certificate_authority(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
536        if self.certificate_authority.is_none() && self.certificate_authority_data.is_none() {
537            return Ok(None);
538        }
539
540        let ca = load_from_base64_or_file(
541            &self.certificate_authority_data.as_deref(),
542            &self.certificate_authority,
543        )
544        .map_err(KubeconfigError::LoadCertificateAuthority)?;
545        Ok(Some(ca))
546    }
547}
548
549impl AuthInfo {
550    pub(crate) fn identity_pem(&self) -> Result<Vec<u8>, KubeconfigError> {
551        let client_cert = &self.load_client_certificate()?;
552        let client_key = &self.load_client_key()?;
553        let mut buffer = client_key.clone();
554        buffer.extend_from_slice(client_cert);
555        Ok(buffer)
556    }
557
558    pub(crate) fn load_client_certificate(&self) -> Result<Vec<u8>, KubeconfigError> {
559        // TODO Shouldn't error when `self.client_certificate_data.is_none() && self.client_certificate.is_none()`
560
561        load_from_base64_or_file(&self.client_certificate_data.as_deref(), &self.client_certificate)
562            .map_err(KubeconfigError::LoadClientCertificate)
563    }
564
565    pub(crate) fn load_client_key(&self) -> Result<Vec<u8>, KubeconfigError> {
566        // TODO Shouldn't error when `self.client_key_data.is_none() && self.client_key.is_none()`
567
568        load_from_base64_or_file(
569            &self.client_key_data.as_ref().map(|secret| secret.expose_secret()),
570            &self.client_key,
571        )
572        .map_err(KubeconfigError::LoadClientKey)
573    }
574}
575
576/// Connection information for auth plugins that have `provideClusterInfo` enabled.
577///
578/// This is a copy of [`kube::config::Cluster`] with certificate_authority passed as bytes without the path.
579/// Taken from [clientauthentication/types.go#Cluster](https://github.com/kubernetes/client-go/blob/477cb782cf024bc70b7239f0dca91e5774811950/pkg/apis/clientauthentication/types.go#L73-L129)
580#[derive(Clone, Debug, Serialize, Deserialize, Default)]
581#[serde(rename_all = "kebab-case")]
582#[cfg_attr(test, derive(PartialEq, Eq))]
583pub struct ExecAuthCluster {
584    /// The address of the kubernetes cluster (https://hostname:port).
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub server: Option<String>,
587    /// Skips the validity check for the server's certificate. This will make your HTTPS connections insecure.
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub insecure_skip_tls_verify: Option<bool>,
590    /// PEM-encoded certificate authority certificates. Overrides `certificate_authority`
591    #[serde(default, skip_serializing_if = "Option::is_none")]
592    #[serde(with = "base64serde")]
593    pub certificate_authority_data: Option<Vec<u8>>,
594    /// URL to the proxy to be used for all requests.
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub proxy_url: Option<String>,
597    /// Name used to check server certificate.
598    ///
599    /// If `tls_server_name` is `None`, the hostname used to contact the server is used.
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub tls_server_name: Option<String>,
602    /// This can be anything
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub config: Option<serde_json::Value>,
605}
606
607impl TryFrom<&Cluster> for ExecAuthCluster {
608    type Error = KubeconfigError;
609
610    fn try_from(cluster: &crate::config::Cluster) -> Result<Self, KubeconfigError> {
611        let certificate_authority_data = cluster.load_certificate_authority()?;
612        Ok(Self {
613            server: cluster.server.clone(),
614            insecure_skip_tls_verify: cluster.insecure_skip_tls_verify,
615            certificate_authority_data,
616            proxy_url: cluster.proxy_url.clone(),
617            tls_server_name: cluster.tls_server_name.clone(),
618            config: cluster.extensions.as_ref().and_then(|extensions| {
619                extensions
620                    .iter()
621                    .find(|extension| extension.name == CLUSTER_EXTENSION_KEY)
622                    .map(|extension| extension.extension.clone())
623            }),
624        })
625    }
626}
627
628fn load_from_base64_or_file<P: AsRef<Path>>(
629    value: &Option<&str>,
630    file: &Option<P>,
631) -> Result<Vec<u8>, LoadDataError> {
632    let data = value
633        .map(load_from_base64)
634        .or_else(|| file.as_ref().map(load_from_file))
635        .unwrap_or_else(|| Err(LoadDataError::NoBase64DataOrFile))?;
636    Ok(ensure_trailing_newline(data))
637}
638
639fn load_from_base64(value: &str) -> Result<Vec<u8>, LoadDataError> {
640    use base64::Engine;
641    base64::engine::general_purpose::STANDARD
642        .decode(value)
643        .map_err(LoadDataError::DecodeBase64)
644}
645
646fn load_from_file<P: AsRef<Path>>(file: &P) -> Result<Vec<u8>, LoadDataError> {
647    fs::read(file).map_err(|source| LoadDataError::ReadFile(source, file.as_ref().into()))
648}
649
650// Ensure there is a trailing newline in the blob
651// Don't bother if the blob is empty
652fn ensure_trailing_newline(mut data: Vec<u8>) -> Vec<u8> {
653    if data.last().map(|end| *end != b'\n').unwrap_or(false) {
654        data.push(b'\n');
655    }
656    data
657}
658
659/// Returns kubeconfig path from `$HOME/.kube/config`.
660fn default_kube_path() -> Option<PathBuf> {
661    // Before Rust 1.85.0, `home_dir` would return wrong results on Windows, usage of the crate
662    // `home` was encouraged (and is what kube-rs did).
663    // Rust 1.85.0 fixed the problem (https://doc.rust-lang.org/1.85.0/std/env/fn.home_dir.html),
664    // Rust 1.87.0 removed the function deprecation.
665    // As the MSRV was bumped to 1.85.0 we are safe to use the fixed std function.
666    std::env::home_dir().map(|h| h.join(".kube").join("config"))
667}
668
669mod base64serde {
670    use base64::Engine;
671    use serde::{Deserialize, Deserializer, Serialize, Serializer};
672
673    pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
674        match v {
675            Some(v) => {
676                let encoded = base64::engine::general_purpose::STANDARD.encode(v);
677                String::serialize(&encoded, s)
678            }
679            None => <Option<String>>::serialize(&None, s),
680        }
681    }
682
683    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
684        let data = <Option<String>>::deserialize(d)?;
685        match data {
686            Some(data) => Ok(Some(
687                base64::engine::general_purpose::STANDARD
688                    .decode(data.as_bytes())
689                    .map_err(serde::de::Error::custom)?,
690            )),
691            None => Ok(None),
692        }
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use crate::config::file_loader::ConfigLoader;
699
700    use super::*;
701    use serde_json::{Value, json};
702
703    #[test]
704    fn kubeconfig_merge() {
705        let kubeconfig1 = Kubeconfig {
706            current_context: Some("default".into()),
707            auth_infos: vec![NamedAuthInfo {
708                name: "red-user".into(),
709                auth_info: Some(AuthInfo {
710                    token: Some(SecretString::new("first-token".into())),
711                    ..Default::default()
712                }),
713            }],
714            ..Default::default()
715        };
716        let kubeconfig2 = Kubeconfig {
717            current_context: Some("dev".into()),
718            auth_infos: vec![
719                NamedAuthInfo {
720                    name: "red-user".into(),
721                    auth_info: Some(AuthInfo {
722                        token: Some(SecretString::new("second-token".into())),
723                        username: Some("red-user".into()),
724                        ..Default::default()
725                    }),
726                },
727                NamedAuthInfo {
728                    name: "green-user".into(),
729                    auth_info: Some(AuthInfo {
730                        token: Some(SecretString::new("new-token".into())),
731                        ..Default::default()
732                    }),
733                },
734            ],
735            ..Default::default()
736        };
737
738        let merged = kubeconfig1.merge(kubeconfig2).unwrap();
739        // Preserves first `current_context`
740        assert_eq!(merged.current_context, Some("default".into()));
741        // Auth info with the same name does not overwrite
742        assert_eq!(merged.auth_infos[0].name, "red-user");
743        assert_eq!(
744            merged.auth_infos[0]
745                .auth_info
746                .as_ref()
747                .unwrap()
748                .token
749                .as_ref()
750                .map(|t| t.expose_secret()),
751            Some("first-token")
752        );
753        // Even if it's not conflicting
754        assert_eq!(merged.auth_infos[0].auth_info.as_ref().unwrap().username, None);
755        // New named auth info is appended
756        assert_eq!(merged.auth_infos[1].name, "green-user");
757    }
758
759    #[test]
760    fn kubeconfig_deserialize() {
761        let config_yaml = "apiVersion: v1
762clusters:
763- cluster:
764    certificate-authority-data: LS0t<SNIP>LS0tLQo=
765    server: https://ABCDEF0123456789.gr7.us-west-2.eks.amazonaws.com
766  name: eks
767- cluster:
768    certificate-authority: /home/kevin/.minikube/ca.crt
769    extensions:
770    - extension:
771        last-update: Thu, 18 Feb 2021 16:59:26 PST
772        provider: minikube.sigs.k8s.io
773        version: v1.17.1
774      name: cluster_info
775    server: https://192.168.49.2:8443
776  name: minikube
777contexts:
778- context:
779    cluster: minikube
780    extensions:
781    - extension:
782        last-update: Thu, 18 Feb 2021 16:59:26 PST
783        provider: minikube.sigs.k8s.io
784        version: v1.17.1
785      name: context_info
786    namespace: default
787    user: minikube
788  name: minikube
789- context:
790    cluster: arn:aws:eks:us-west-2:012345678912:cluster/eks
791    user: arn:aws:eks:us-west-2:012345678912:cluster/eks
792  name: eks
793current-context: minikube
794kind: Config
795preferences: {}
796users:
797- name: arn:aws:eks:us-west-2:012345678912:cluster/eks
798  user:
799    exec:
800      apiVersion: client.authentication.k8s.io/v1alpha1
801      args:
802      - --region
803      - us-west-2
804      - eks
805      - get-token
806      - --cluster-name
807      - eks
808      command: aws
809      env: null
810      provideClusterInfo: false
811- name: minikube
812  user:
813    client-certificate: /home/kevin/.minikube/profiles/minikube/client.crt
814    client-key: /home/kevin/.minikube/profiles/minikube/client.key";
815
816        let config = Kubeconfig::from_yaml(config_yaml).unwrap();
817
818        assert_eq!(config.clusters[0].name, "eks");
819        assert_eq!(config.clusters[1].name, "minikube");
820
821        let cluster1 = config.clusters[1].cluster.as_ref().unwrap();
822        assert_eq!(
823            cluster1.extensions.as_ref().unwrap()[0].extension.get("provider"),
824            Some(&Value::String("minikube.sigs.k8s.io".to_owned()))
825        );
826    }
827
828    #[test]
829    fn kubeconfig_multi_document_merge() -> Result<(), KubeconfigError> {
830        let config_yaml = r#"---
831apiVersion: v1
832clusters:
833- cluster:
834    certificate-authority-data: aGVsbG8K
835    server: https://0.0.0.0:6443
836  name: k3d-promstack
837contexts:
838- context:
839    cluster: k3d-promstack
840    user: admin@k3d-promstack
841  name: k3d-promstack
842current-context: k3d-promstack
843kind: Config
844preferences: {}
845users:
846- name: admin@k3d-promstack
847  user:
848    client-certificate-data: aGVsbG8K
849    client-key-data: aGVsbG8K
850---
851apiVersion: v1
852clusters:
853- cluster:
854    certificate-authority-data: aGVsbG8K
855    server: https://0.0.0.0:6443
856  name: k3d-k3s-default
857contexts:
858- context:
859    cluster: k3d-k3s-default
860    user: admin@k3d-k3s-default
861  name: k3d-k3s-default
862current-context: k3d-k3s-default
863kind: Config
864preferences: {}
865users:
866- name: admin@k3d-k3s-default
867  user:
868    client-certificate-data: aGVsbG8K
869    client-key-data: aGVsbG8K
870"#;
871        let cfg = Kubeconfig::from_yaml(config_yaml)?;
872
873        // Ensure we have data from both documents:
874        assert_eq!(cfg.clusters[0].name, "k3d-promstack");
875        assert_eq!(cfg.clusters[1].name, "k3d-k3s-default");
876
877        Ok(())
878    }
879
880    #[test]
881    fn kubeconfig_split_sections_merge() -> Result<(), KubeconfigError> {
882        let config1 = r#"
883apiVersion: v1
884clusters:
885- cluster:
886    certificate-authority-data: aGVsbG8K
887    server: https://0.0.0.0:6443
888  name: k3d-promstack
889contexts:
890- context:
891    cluster: k3d-promstack
892    user: admin@k3d-promstack
893  name: k3d-promstack
894current-context: k3d-promstack
895kind: Config
896preferences: {}
897"#;
898
899        let config2 = r#"
900users:
901- name: admin@k3d-k3s-default
902  user:
903    client-certificate-data: aGVsbG8K
904    client-key-data: aGVsbG8K
905"#;
906
907        let kubeconfig1 = Kubeconfig::from_yaml(config1)?;
908        let kubeconfig2 = Kubeconfig::from_yaml(config2)?;
909        let merged = kubeconfig1.merge(kubeconfig2).unwrap();
910
911        // Ensure we have data from both files:
912        assert_eq!(merged.clusters[0].name, "k3d-promstack");
913        assert_eq!(merged.contexts[0].name, "k3d-promstack");
914        assert_eq!(merged.auth_infos[0].name, "admin@k3d-k3s-default");
915
916        Ok(())
917    }
918
919    #[test]
920    fn kubeconfig_from_empty_string() {
921        let cfg = Kubeconfig::from_yaml("").unwrap();
922
923        assert_eq!(cfg, Kubeconfig::default());
924    }
925
926    #[test]
927    fn authinfo_deserialize_null_secret() {
928        let authinfo_yaml = r#"
929username: user
930password: 
931"#;
932        let authinfo: AuthInfo = serde_yaml::from_str(authinfo_yaml).unwrap();
933        assert_eq!(authinfo.username, Some("user".to_string()));
934        assert!(authinfo.password.is_none());
935    }
936
937    #[test]
938    fn authinfo_debug_does_not_output_password() {
939        let authinfo_yaml = r#"
940username: user
941password: kube_rs
942"#;
943        let authinfo: AuthInfo = serde_yaml::from_str(authinfo_yaml).unwrap();
944        let authinfo_debug_output = format!("{authinfo:?}");
945        let expected_output = "AuthInfo { \
946        username: Some(\"user\"), \
947        password: Some(SecretBox<str>([REDACTED])), \
948        token: None, token_file: None, client_certificate: None, \
949        client_certificate_data: None, client_key: None, \
950        client_key_data: None, impersonate: None, \
951        impersonate_groups: None, \
952        auth_provider: None, \
953        exec: None \
954        }";
955
956        assert_eq!(authinfo_debug_output, expected_output)
957    }
958
959    #[tokio::test]
960    async fn authinfo_exec_provide_cluster_info() {
961        let config = r#"
962apiVersion: v1
963clusters:
964- cluster:
965    server: https://localhost:8080
966    extensions:
967    - name: client.authentication.k8s.io/exec
968      extension:
969        audience: foo
970        other: bar
971  name: foo-cluster
972contexts:
973- context:
974    cluster: foo-cluster
975    user: foo-user
976    namespace: bar
977  name: foo-context
978current-context: foo-context
979kind: Config
980users:
981- name: foo-user
982  user:
983    exec:
984      apiVersion: client.authentication.k8s.io/v1alpha1
985      args:
986      - arg-1
987      - arg-2
988      command: foo-command
989      provideClusterInfo: true
990"#;
991        let kube_config = Kubeconfig::from_yaml(config).unwrap();
992        let config_loader = ConfigLoader::load(kube_config, None, None, None).await.unwrap();
993        let auth_info = config_loader.user;
994        let exec = auth_info.exec.unwrap();
995        assert!(exec.provide_cluster_info);
996        let cluster = exec.cluster.unwrap();
997        assert_eq!(
998            cluster.config.unwrap(),
999            json!({"audience": "foo", "other": "bar"})
1000        );
1001    }
1002
1003    #[tokio::test]
1004    async fn parse_kubeconfig_encodings() {
1005        let files = vec![
1006            "kubeconfig_utf8.yaml",
1007            "kubeconfig_utf16le.yaml",
1008            "kubeconfig_utf16be.yaml",
1009        ];
1010
1011        for file_name in files {
1012            let path = PathBuf::from(format!(
1013                "{}/src/config/test_data/{}",
1014                env!("CARGO_MANIFEST_DIR"),
1015                file_name
1016            ));
1017            let cfg = Kubeconfig::read_from(path).unwrap();
1018            assert_eq!(cfg.clusters[0].name, "k3d-promstack");
1019            assert_eq!(cfg.contexts[0].name, "k3d-promstack");
1020            assert_eq!(cfg.auth_infos[0].name, "admin@k3d-k3s-default");
1021        }
1022    }
1023}