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
12const CLUSTER_EXTENSION_KEY: &str = "client.authentication.k8s.io/exec";
14
15#[derive(Clone, Debug, Serialize, Deserialize, Default)]
25#[cfg_attr(test, derive(PartialEq))]
26pub struct Kubeconfig {
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub preferences: Option<Preferences>,
30 #[serde(default, deserialize_with = "deserialize_null_as_default")]
32 pub clusters: Vec<NamedCluster>,
33 #[serde(rename = "users")]
35 #[serde(default, deserialize_with = "deserialize_null_as_default")]
36 pub auth_infos: Vec<NamedAuthInfo>,
37 #[serde(default, deserialize_with = "deserialize_null_as_default")]
39 pub contexts: Vec<NamedContext>,
40 #[serde(rename = "current-context")]
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub current_context: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub extensions: Option<Vec<NamedExtension>>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
51 pub kind: Option<String>,
52 #[serde(rename = "apiVersion")]
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub api_version: Option<String>,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
60#[cfg_attr(test, derive(PartialEq, Eq))]
61pub struct Preferences {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub colors: Option<bool>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub extensions: Option<Vec<NamedExtension>>,
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize)]
72#[cfg_attr(test, derive(PartialEq, Eq))]
73pub struct NamedExtension {
74 pub name: String,
76 pub extension: serde_json::Value,
78}
79
80#[derive(Clone, Debug, Serialize, Deserialize, Default)]
82#[cfg_attr(test, derive(PartialEq, Eq))]
83pub struct NamedCluster {
84 pub name: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub cluster: Option<Cluster>,
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize, Default)]
93#[cfg_attr(test, derive(PartialEq, Eq))]
94pub struct Cluster {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub server: Option<String>,
98 #[serde(rename = "insecure-skip-tls-verify")]
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub insecure_skip_tls_verify: Option<bool>,
102 #[serde(rename = "certificate-authority")]
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub certificate_authority: Option<String>,
106 #[serde(rename = "certificate-authority-data")]
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub certificate_authority_data: Option<String>,
110 #[serde(rename = "proxy-url")]
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub proxy_url: Option<String>,
114 #[serde(rename = "disable-compression")]
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub disable_compression: Option<bool>,
122 #[serde(rename = "tls-server-name")]
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub tls_server_name: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub extensions: Option<Vec<NamedExtension>>,
131}
132
133#[derive(Clone, Debug, Serialize, Deserialize, Default)]
135#[cfg_attr(test, derive(PartialEq))]
136pub struct NamedAuthInfo {
137 pub name: String,
139 #[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#[derive(Clone, Debug, Serialize, Deserialize, Default)]
177pub struct AuthInfo {
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub username: Option<String>,
181 #[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 #[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 #[serde(rename = "tokenFile")]
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub token_file: Option<String>,
200
201 #[serde(rename = "client-certificate")]
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub client_certificate: Option<String>,
205 #[serde(rename = "client-certificate-data")]
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub client_certificate_data: Option<String>,
210
211 #[serde(rename = "client-key")]
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub client_key: Option<String>,
215 #[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 #[serde(rename = "as")]
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub impersonate: Option<String>,
229 #[serde(rename = "as-groups")]
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub impersonate_groups: Option<Vec<String>>,
233
234 #[serde(rename = "auth-provider")]
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub auth_provider: Option<AuthProviderConfig>,
238
239 #[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#[derive(Clone, Debug, Serialize, Deserialize)]
253#[cfg_attr(test, derive(PartialEq, Eq))]
254pub struct AuthProviderConfig {
255 pub name: String,
257 #[serde(default)]
259 pub config: HashMap<String, String>,
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize)]
264#[cfg_attr(test, derive(PartialEq, Eq))]
265pub struct ExecConfig {
266 #[serde(rename = "apiVersion")]
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub api_version: Option<String>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub command: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub args: Option<Vec<String>>,
278 #[serde(skip_serializing_if = "Option::is_none")]
282 pub env: Option<Vec<HashMap<String, String>>>,
283 #[serde(skip)]
288 pub drop_env: Option<Vec<String>>,
289
290 #[serde(rename = "interactiveMode")]
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub interactive_mode: Option<ExecInteractiveMode>,
294
295 #[serde(default, rename = "provideClusterInfo")]
301 pub provide_cluster_info: bool,
302
303 #[serde(skip)]
306 pub cluster: Option<ExecAuthCluster>,
307}
308
309#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
311#[cfg_attr(test, derive(Eq))]
312pub enum ExecInteractiveMode {
313 Never,
315 IfAvailable,
317 Always,
319}
320
321#[derive(Clone, Debug, Serialize, Deserialize, Default)]
323#[cfg_attr(test, derive(PartialEq, Eq))]
324pub struct NamedContext {
325 pub name: String,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub context: Option<Context>,
330}
331
332#[derive(Clone, Debug, Serialize, Deserialize, Default)]
334#[cfg_attr(test, derive(PartialEq, Eq))]
335pub struct Context {
336 pub cluster: String,
338 pub user: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub namespace: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub extensions: Option<Vec<NamedExtension>>,
346}
347
348const KUBECONFIG: &str = "KUBECONFIG";
349
350impl Kubeconfig {
352 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 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 if let Some(path) = &cluster.certificate_authority {
364 if let Some(abs_path) = to_absolute(dir, path) {
365 cluster.certificate_authority = Some(abs_path);
366 }
367 }
368 }
369 }
370 for named in config.auth_infos.iter_mut() {
371 if let Some(auth_info) = &mut named.auth_info {
372 if let Some(path) = &auth_info.client_certificate {
373 if let Some(abs_path) = to_absolute(dir, path) {
374 auth_info.client_certificate = Some(abs_path);
375 }
376 }
377 if let Some(path) = &auth_info.client_key {
378 if let Some(abs_path) = to_absolute(dir, path) {
379 auth_info.client_key = Some(abs_path);
380 }
381 }
382 if let Some(path) = &auth_info.token_file {
383 if let Some(abs_path) = to_absolute(dir, path) {
384 auth_info.token_file = Some(abs_path);
385 }
386 }
387 }
388 }
389 }
390 if let Some(c) = merged_docs {
391 merged_docs = Some(Kubeconfig::merge(c, config)?);
392 } else {
393 merged_docs = Some(config);
394 }
395 }
396 Ok(merged_docs.unwrap_or_default())
398 }
399
400 pub fn from_yaml(text: &str) -> Result<Kubeconfig, KubeconfigError> {
405 kubeconfig_from_yaml(text)?
406 .into_iter()
407 .try_fold(Kubeconfig::default(), Kubeconfig::merge)
408 }
409
410 pub fn read() -> Result<Kubeconfig, KubeconfigError> {
412 match Self::from_env()? {
413 Some(config) => Ok(config),
414 None => Self::read_from(default_kube_path().ok_or(KubeconfigError::FindPath)?),
415 }
416 }
417
418 pub fn from_env() -> Result<Option<Self>, KubeconfigError> {
425 match std::env::var_os(KUBECONFIG) {
426 Some(value) => {
427 let paths = std::env::split_paths(&value)
428 .filter(|p| !p.as_os_str().is_empty())
429 .collect::<Vec<_>>();
430 if paths.is_empty() {
431 return Ok(None);
432 }
433
434 let merged = paths.iter().try_fold(Kubeconfig::default(), |m, p| {
435 Kubeconfig::read_from(p).and_then(|c| m.merge(c))
436 })?;
437 Ok(Some(merged))
438 }
439
440 None => Ok(None),
441 }
442 }
443
444 pub fn merge(mut self, next: Kubeconfig) -> Result<Self, KubeconfigError> {
457 if self.kind.is_some() && next.kind.is_some() && self.kind != next.kind {
458 return Err(KubeconfigError::KindMismatch);
459 }
460 if self.api_version.is_some() && next.api_version.is_some() && self.api_version != next.api_version {
461 return Err(KubeconfigError::ApiVersionMismatch);
462 }
463
464 self.kind = self.kind.or(next.kind);
465 self.api_version = self.api_version.or(next.api_version);
466 self.preferences = self.preferences.or(next.preferences);
467 append_new_named(&mut self.clusters, next.clusters, |x| &x.name);
468 append_new_named(&mut self.auth_infos, next.auth_infos, |x| &x.name);
469 append_new_named(&mut self.contexts, next.contexts, |x| &x.name);
470 self.current_context = self.current_context.or(next.current_context);
471 self.extensions = self.extensions.or(next.extensions);
472 Ok(self)
473 }
474}
475
476fn kubeconfig_from_yaml(text: &str) -> Result<Vec<Kubeconfig>, KubeconfigError> {
477 let mut documents = vec![];
478 for doc in serde_yaml::Deserializer::from_str(text) {
479 let value = serde_yaml::Value::deserialize(doc).map_err(KubeconfigError::Parse)?;
480 let kubeconfig = serde_yaml::from_value(value).map_err(KubeconfigError::InvalidStructure)?;
481 documents.push(kubeconfig);
482 }
483 Ok(documents)
484}
485
486#[allow(clippy::redundant_closure)]
487fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
488where
489 F: Fn(&T) -> &String,
490{
491 use std::collections::HashSet;
492 base.extend({
493 let existing = base.iter().map(|x| f(x)).collect::<HashSet<_>>();
494 next.into_iter()
495 .filter(|x| !existing.contains(f(x)))
496 .collect::<Vec<_>>()
497 });
498}
499
500fn read_path<P: AsRef<Path>>(path: P) -> io::Result<String> {
501 let bytes = fs::read(&path)?;
502 match bytes.as_slice() {
503 [0xFF, 0xFE, ..] => {
504 let utf16_data: Vec<u16> = bytes[2..]
505 .chunks(2)
506 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
507 .collect();
508 String::from_utf16(&utf16_data)
509 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 LE"))
510 }
511 [0xFE, 0xFF, ..] => {
512 let utf16_data: Vec<u16> = bytes[2..]
513 .chunks(2)
514 .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
515 .collect();
516 String::from_utf16(&utf16_data)
517 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 BE"))
518 }
519 [0xEF, 0xBB, 0xBF, ..] => String::from_utf8(bytes[3..].to_vec())
520 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 BOM")),
521 _ => {
522 String::from_utf8(bytes).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8"))
523 }
524 }
525}
526
527fn to_absolute(dir: &Path, file: &str) -> Option<String> {
528 let path = Path::new(&file);
529 if path.is_relative() {
530 dir.join(path).to_str().map(str::to_owned)
531 } else {
532 None
533 }
534}
535
536impl Cluster {
537 pub(crate) fn load_certificate_authority(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
538 if self.certificate_authority.is_none() && self.certificate_authority_data.is_none() {
539 return Ok(None);
540 }
541
542 let ca = load_from_base64_or_file(
543 &self.certificate_authority_data.as_deref(),
544 &self.certificate_authority,
545 )
546 .map_err(KubeconfigError::LoadCertificateAuthority)?;
547 Ok(Some(ca))
548 }
549}
550
551impl AuthInfo {
552 pub(crate) fn identity_pem(&self) -> Result<Vec<u8>, KubeconfigError> {
553 let client_cert = &self.load_client_certificate()?;
554 let client_key = &self.load_client_key()?;
555 let mut buffer = client_key.clone();
556 buffer.extend_from_slice(client_cert);
557 Ok(buffer)
558 }
559
560 pub(crate) fn load_client_certificate(&self) -> Result<Vec<u8>, KubeconfigError> {
561 load_from_base64_or_file(&self.client_certificate_data.as_deref(), &self.client_certificate)
564 .map_err(KubeconfigError::LoadClientCertificate)
565 }
566
567 pub(crate) fn load_client_key(&self) -> Result<Vec<u8>, KubeconfigError> {
568 load_from_base64_or_file(
571 &self.client_key_data.as_ref().map(|secret| secret.expose_secret()),
572 &self.client_key,
573 )
574 .map_err(KubeconfigError::LoadClientKey)
575 }
576}
577
578#[derive(Clone, Debug, Serialize, Deserialize, Default)]
583#[serde(rename_all = "kebab-case")]
584#[cfg_attr(test, derive(PartialEq, Eq))]
585pub struct ExecAuthCluster {
586 #[serde(skip_serializing_if = "Option::is_none")]
588 pub server: Option<String>,
589 #[serde(skip_serializing_if = "Option::is_none")]
591 pub insecure_skip_tls_verify: Option<bool>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
594 #[serde(with = "base64serde")]
595 pub certificate_authority_data: Option<Vec<u8>>,
596 #[serde(skip_serializing_if = "Option::is_none")]
598 pub proxy_url: Option<String>,
599 #[serde(skip_serializing_if = "Option::is_none")]
603 pub tls_server_name: Option<String>,
604 #[serde(skip_serializing_if = "Option::is_none")]
606 pub config: Option<serde_json::Value>,
607}
608
609impl TryFrom<&Cluster> for ExecAuthCluster {
610 type Error = KubeconfigError;
611
612 fn try_from(cluster: &crate::config::Cluster) -> Result<Self, KubeconfigError> {
613 let certificate_authority_data = cluster.load_certificate_authority()?;
614 Ok(Self {
615 server: cluster.server.clone(),
616 insecure_skip_tls_verify: cluster.insecure_skip_tls_verify,
617 certificate_authority_data,
618 proxy_url: cluster.proxy_url.clone(),
619 tls_server_name: cluster.tls_server_name.clone(),
620 config: cluster.extensions.as_ref().and_then(|extensions| {
621 extensions
622 .iter()
623 .find(|extension| extension.name == CLUSTER_EXTENSION_KEY)
624 .map(|extension| extension.extension.clone())
625 }),
626 })
627 }
628}
629
630fn load_from_base64_or_file<P: AsRef<Path>>(
631 value: &Option<&str>,
632 file: &Option<P>,
633) -> Result<Vec<u8>, LoadDataError> {
634 let data = value
635 .map(load_from_base64)
636 .or_else(|| file.as_ref().map(load_from_file))
637 .unwrap_or_else(|| Err(LoadDataError::NoBase64DataOrFile))?;
638 Ok(ensure_trailing_newline(data))
639}
640
641fn load_from_base64(value: &str) -> Result<Vec<u8>, LoadDataError> {
642 use base64::Engine;
643 base64::engine::general_purpose::STANDARD
644 .decode(value)
645 .map_err(LoadDataError::DecodeBase64)
646}
647
648fn load_from_file<P: AsRef<Path>>(file: &P) -> Result<Vec<u8>, LoadDataError> {
649 fs::read(file).map_err(|source| LoadDataError::ReadFile(source, file.as_ref().into()))
650}
651
652fn ensure_trailing_newline(mut data: Vec<u8>) -> Vec<u8> {
655 if data.last().map(|end| *end != b'\n').unwrap_or(false) {
656 data.push(b'\n');
657 }
658 data
659}
660
661fn default_kube_path() -> Option<PathBuf> {
663 home::home_dir().map(|h| h.join(".kube").join("config"))
664}
665
666mod base64serde {
667 use base64::Engine;
668 use serde::{Deserialize, Deserializer, Serialize, Serializer};
669
670 pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
671 match v {
672 Some(v) => {
673 let encoded = base64::engine::general_purpose::STANDARD.encode(v);
674 String::serialize(&encoded, s)
675 }
676 None => <Option<String>>::serialize(&None, s),
677 }
678 }
679
680 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
681 let data = <Option<String>>::deserialize(d)?;
682 match data {
683 Some(data) => Ok(Some(
684 base64::engine::general_purpose::STANDARD
685 .decode(data.as_bytes())
686 .map_err(serde::de::Error::custom)?,
687 )),
688 None => Ok(None),
689 }
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use crate::config::file_loader::ConfigLoader;
696
697 use super::*;
698 use serde_json::{json, Value};
699
700 #[test]
701 fn kubeconfig_merge() {
702 let kubeconfig1 = Kubeconfig {
703 current_context: Some("default".into()),
704 auth_infos: vec![NamedAuthInfo {
705 name: "red-user".into(),
706 auth_info: Some(AuthInfo {
707 token: Some(SecretString::new("first-token".into())),
708 ..Default::default()
709 }),
710 }],
711 ..Default::default()
712 };
713 let kubeconfig2 = Kubeconfig {
714 current_context: Some("dev".into()),
715 auth_infos: vec![
716 NamedAuthInfo {
717 name: "red-user".into(),
718 auth_info: Some(AuthInfo {
719 token: Some(SecretString::new("second-token".into())),
720 username: Some("red-user".into()),
721 ..Default::default()
722 }),
723 },
724 NamedAuthInfo {
725 name: "green-user".into(),
726 auth_info: Some(AuthInfo {
727 token: Some(SecretString::new("new-token".into())),
728 ..Default::default()
729 }),
730 },
731 ],
732 ..Default::default()
733 };
734
735 let merged = kubeconfig1.merge(kubeconfig2).unwrap();
736 assert_eq!(merged.current_context, Some("default".into()));
738 assert_eq!(merged.auth_infos[0].name, "red-user");
740 assert_eq!(
741 merged.auth_infos[0]
742 .auth_info
743 .as_ref()
744 .unwrap()
745 .token
746 .as_ref()
747 .map(|t| t.expose_secret()),
748 Some("first-token")
749 );
750 assert_eq!(merged.auth_infos[0].auth_info.as_ref().unwrap().username, None);
752 assert_eq!(merged.auth_infos[1].name, "green-user");
754 }
755
756 #[test]
757 fn kubeconfig_deserialize() {
758 let config_yaml = "apiVersion: v1
759clusters:
760- cluster:
761 certificate-authority-data: LS0t<SNIP>LS0tLQo=
762 server: https://ABCDEF0123456789.gr7.us-west-2.eks.amazonaws.com
763 name: eks
764- cluster:
765 certificate-authority: /home/kevin/.minikube/ca.crt
766 extensions:
767 - extension:
768 last-update: Thu, 18 Feb 2021 16:59:26 PST
769 provider: minikube.sigs.k8s.io
770 version: v1.17.1
771 name: cluster_info
772 server: https://192.168.49.2:8443
773 name: minikube
774contexts:
775- context:
776 cluster: minikube
777 extensions:
778 - extension:
779 last-update: Thu, 18 Feb 2021 16:59:26 PST
780 provider: minikube.sigs.k8s.io
781 version: v1.17.1
782 name: context_info
783 namespace: default
784 user: minikube
785 name: minikube
786- context:
787 cluster: arn:aws:eks:us-west-2:012345678912:cluster/eks
788 user: arn:aws:eks:us-west-2:012345678912:cluster/eks
789 name: eks
790current-context: minikube
791kind: Config
792preferences: {}
793users:
794- name: arn:aws:eks:us-west-2:012345678912:cluster/eks
795 user:
796 exec:
797 apiVersion: client.authentication.k8s.io/v1alpha1
798 args:
799 - --region
800 - us-west-2
801 - eks
802 - get-token
803 - --cluster-name
804 - eks
805 command: aws
806 env: null
807 provideClusterInfo: false
808- name: minikube
809 user:
810 client-certificate: /home/kevin/.minikube/profiles/minikube/client.crt
811 client-key: /home/kevin/.minikube/profiles/minikube/client.key";
812
813 let config = Kubeconfig::from_yaml(config_yaml).unwrap();
814
815 assert_eq!(config.clusters[0].name, "eks");
816 assert_eq!(config.clusters[1].name, "minikube");
817
818 let cluster1 = config.clusters[1].cluster.as_ref().unwrap();
819 assert_eq!(
820 cluster1.extensions.as_ref().unwrap()[0].extension.get("provider"),
821 Some(&Value::String("minikube.sigs.k8s.io".to_owned()))
822 );
823 }
824
825 #[test]
826 fn kubeconfig_multi_document_merge() -> Result<(), KubeconfigError> {
827 let config_yaml = r#"---
828apiVersion: v1
829clusters:
830- cluster:
831 certificate-authority-data: aGVsbG8K
832 server: https://0.0.0.0:6443
833 name: k3d-promstack
834contexts:
835- context:
836 cluster: k3d-promstack
837 user: admin@k3d-promstack
838 name: k3d-promstack
839current-context: k3d-promstack
840kind: Config
841preferences: {}
842users:
843- name: admin@k3d-promstack
844 user:
845 client-certificate-data: aGVsbG8K
846 client-key-data: aGVsbG8K
847---
848apiVersion: v1
849clusters:
850- cluster:
851 certificate-authority-data: aGVsbG8K
852 server: https://0.0.0.0:6443
853 name: k3d-k3s-default
854contexts:
855- context:
856 cluster: k3d-k3s-default
857 user: admin@k3d-k3s-default
858 name: k3d-k3s-default
859current-context: k3d-k3s-default
860kind: Config
861preferences: {}
862users:
863- name: admin@k3d-k3s-default
864 user:
865 client-certificate-data: aGVsbG8K
866 client-key-data: aGVsbG8K
867"#;
868 let cfg = Kubeconfig::from_yaml(config_yaml)?;
869
870 assert_eq!(cfg.clusters[0].name, "k3d-promstack");
872 assert_eq!(cfg.clusters[1].name, "k3d-k3s-default");
873
874 Ok(())
875 }
876
877 #[test]
878 fn kubeconfig_split_sections_merge() -> Result<(), KubeconfigError> {
879 let config1 = r#"
880apiVersion: v1
881clusters:
882- cluster:
883 certificate-authority-data: aGVsbG8K
884 server: https://0.0.0.0:6443
885 name: k3d-promstack
886contexts:
887- context:
888 cluster: k3d-promstack
889 user: admin@k3d-promstack
890 name: k3d-promstack
891current-context: k3d-promstack
892kind: Config
893preferences: {}
894"#;
895
896 let config2 = r#"
897users:
898- name: admin@k3d-k3s-default
899 user:
900 client-certificate-data: aGVsbG8K
901 client-key-data: aGVsbG8K
902"#;
903
904 let kubeconfig1 = Kubeconfig::from_yaml(config1)?;
905 let kubeconfig2 = Kubeconfig::from_yaml(config2)?;
906 let merged = kubeconfig1.merge(kubeconfig2).unwrap();
907
908 assert_eq!(merged.clusters[0].name, "k3d-promstack");
910 assert_eq!(merged.contexts[0].name, "k3d-promstack");
911 assert_eq!(merged.auth_infos[0].name, "admin@k3d-k3s-default");
912
913 Ok(())
914 }
915
916 #[test]
917 fn kubeconfig_from_empty_string() {
918 let cfg = Kubeconfig::from_yaml("").unwrap();
919
920 assert_eq!(cfg, Kubeconfig::default());
921 }
922
923 #[test]
924 fn authinfo_deserialize_null_secret() {
925 let authinfo_yaml = r#"
926username: user
927password:
928"#;
929 let authinfo: AuthInfo = serde_yaml::from_str(authinfo_yaml).unwrap();
930 assert_eq!(authinfo.username, Some("user".to_string()));
931 assert!(authinfo.password.is_none());
932 }
933
934 #[test]
935 fn authinfo_debug_does_not_output_password() {
936 let authinfo_yaml = r#"
937username: user
938password: kube_rs
939"#;
940 let authinfo: AuthInfo = serde_yaml::from_str(authinfo_yaml).unwrap();
941 let authinfo_debug_output = format!("{authinfo:?}");
942 let expected_output = "AuthInfo { \
943 username: Some(\"user\"), \
944 password: Some(SecretBox<str>([REDACTED])), \
945 token: None, token_file: None, client_certificate: None, \
946 client_certificate_data: None, client_key: None, \
947 client_key_data: None, impersonate: None, \
948 impersonate_groups: None, \
949 auth_provider: None, \
950 exec: None \
951 }";
952
953 assert_eq!(authinfo_debug_output, expected_output)
954 }
955
956 #[tokio::test]
957 async fn authinfo_exec_provide_cluster_info() {
958 let config = r#"
959apiVersion: v1
960clusters:
961- cluster:
962 server: https://localhost:8080
963 extensions:
964 - name: client.authentication.k8s.io/exec
965 extension:
966 audience: foo
967 other: bar
968 name: foo-cluster
969contexts:
970- context:
971 cluster: foo-cluster
972 user: foo-user
973 namespace: bar
974 name: foo-context
975current-context: foo-context
976kind: Config
977users:
978- name: foo-user
979 user:
980 exec:
981 apiVersion: client.authentication.k8s.io/v1alpha1
982 args:
983 - arg-1
984 - arg-2
985 command: foo-command
986 provideClusterInfo: true
987"#;
988 let kube_config = Kubeconfig::from_yaml(config).unwrap();
989 let config_loader = ConfigLoader::load(kube_config, None, None, None).await.unwrap();
990 let auth_info = config_loader.user;
991 let exec = auth_info.exec.unwrap();
992 assert!(exec.provide_cluster_info);
993 let cluster = exec.cluster.unwrap();
994 assert_eq!(
995 cluster.config.unwrap(),
996 json!({"audience": "foo", "other": "bar"})
997 );
998 }
999
1000 #[tokio::test]
1001 async fn parse_kubeconfig_encodings() {
1002 let files = vec![
1003 "kubeconfig_utf8.yaml",
1004 "kubeconfig_utf16le.yaml",
1005 "kubeconfig_utf16be.yaml",
1006 ];
1007
1008 for file_name in files {
1009 let path = PathBuf::from(format!(
1010 "{}/src/config/test_data/{}",
1011 env!("CARGO_MANIFEST_DIR"),
1012 file_name
1013 ));
1014 let cfg = Kubeconfig::read_from(path).unwrap();
1015 assert_eq!(cfg.clusters[0].name, "k3d-promstack");
1016 assert_eq!(cfg.contexts[0].name, "k3d-promstack");
1017 assert_eq!(cfg.auth_infos[0].name, "admin@k3d-k3s-default");
1018 }
1019 }
1020}