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 && 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 Ok(merged_docs.unwrap_or_default())
397 }
398
399 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 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 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 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 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 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#[derive(Clone, Debug, Serialize, Deserialize, Default)]
581#[serde(rename_all = "kebab-case")]
582#[cfg_attr(test, derive(PartialEq, Eq))]
583pub struct ExecAuthCluster {
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub server: Option<String>,
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub insecure_skip_tls_verify: Option<bool>,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
592 #[serde(with = "base64serde")]
593 pub certificate_authority_data: Option<Vec<u8>>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub proxy_url: Option<String>,
597 #[serde(skip_serializing_if = "Option::is_none")]
601 pub tls_server_name: Option<String>,
602 #[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
650fn 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
659fn default_kube_path() -> Option<PathBuf> {
661 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 assert_eq!(merged.current_context, Some("default".into()));
741 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 assert_eq!(merged.auth_infos[0].auth_info.as_ref().unwrap().username, None);
755 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 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 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}