1use std::{
2 collections::{BTreeMap, 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 #[serde(flatten)]
62 pub other: BTreeMap<String, serde_json::Value>,
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize, Default)]
67#[cfg_attr(test, derive(PartialEq, Eq))]
68pub struct Preferences {
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub colors: Option<bool>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub extensions: Option<Vec<NamedExtension>>,
75
76 #[serde(flatten)]
81 pub other: BTreeMap<String, serde_json::Value>,
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize)]
86#[cfg_attr(test, derive(PartialEq, Eq))]
87pub struct NamedExtension {
88 pub name: String,
90 pub extension: serde_json::Value,
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize, Default)]
96#[cfg_attr(test, derive(PartialEq, Eq))]
97pub struct NamedCluster {
98 pub name: String,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub cluster: Option<Cluster>,
103
104 #[serde(flatten)]
109 pub other: BTreeMap<String, serde_json::Value>,
110}
111
112#[derive(Clone, Debug, Serialize, Deserialize, Default)]
114#[cfg_attr(test, derive(PartialEq, Eq))]
115pub struct Cluster {
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub server: Option<String>,
119 #[serde(rename = "insecure-skip-tls-verify")]
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub insecure_skip_tls_verify: Option<bool>,
123 #[serde(rename = "certificate-authority")]
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub certificate_authority: Option<String>,
127 #[serde(rename = "certificate-authority-data")]
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub certificate_authority_data: Option<String>,
131 #[serde(rename = "proxy-url")]
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub proxy_url: Option<String>,
135 #[serde(rename = "disable-compression")]
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub disable_compression: Option<bool>,
143 #[serde(rename = "tls-server-name")]
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub tls_server_name: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub extensions: Option<Vec<NamedExtension>>,
152
153 #[serde(flatten)]
158 pub other: BTreeMap<String, serde_json::Value>,
159}
160
161#[derive(Clone, Debug, Serialize, Deserialize, Default)]
163#[cfg_attr(test, derive(PartialEq))]
164pub struct NamedAuthInfo {
165 pub name: String,
167 #[serde(rename = "user")]
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub auth_info: Option<AuthInfo>,
171
172 #[serde(flatten)]
177 pub other: BTreeMap<String, serde_json::Value>,
178}
179
180fn serialize_secretstring<S>(pw: &Option<SecretString>, serializer: S) -> Result<S::Ok, S::Error>
181where
182 S: Serializer,
183{
184 match pw {
185 Some(secret) => serializer.serialize_str(secret.expose_secret()),
186 None => serializer.serialize_none(),
187 }
188}
189
190fn deserialize_secretstring<'de, D>(deserializer: D) -> Result<Option<SecretString>, D::Error>
191where
192 D: Deserializer<'de>,
193{
194 match Option::<String>::deserialize(deserializer) {
195 Ok(Some(secret)) => Ok(Some(SecretString::new(secret.into()))),
196 Ok(None) => Ok(None),
197 Err(e) => Err(e),
198 }
199}
200
201fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
202where
203 T: Default + Deserialize<'de>,
204 D: Deserializer<'de>,
205{
206 let opt = Option::deserialize(deserializer)?;
207 Ok(opt.unwrap_or_default())
208}
209
210#[derive(Clone, Debug, Serialize, Deserialize, Default)]
212pub struct AuthInfo {
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub username: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none", default)]
218 #[serde(
219 serialize_with = "serialize_secretstring",
220 deserialize_with = "deserialize_secretstring"
221 )]
222 pub password: Option<SecretString>,
223
224 #[serde(skip_serializing_if = "Option::is_none", default)]
226 #[serde(
227 serialize_with = "serialize_secretstring",
228 deserialize_with = "deserialize_secretstring"
229 )]
230 pub token: Option<SecretString>,
231 #[serde(rename = "tokenFile")]
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub token_file: Option<String>,
235
236 #[serde(rename = "client-certificate")]
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub client_certificate: Option<String>,
240 #[serde(rename = "client-certificate-data")]
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub client_certificate_data: Option<String>,
245
246 #[serde(rename = "client-key")]
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub client_key: Option<String>,
250 #[serde(rename = "client-key-data")]
253 #[serde(skip_serializing_if = "Option::is_none", default)]
254 #[serde(
255 serialize_with = "serialize_secretstring",
256 deserialize_with = "deserialize_secretstring"
257 )]
258 pub client_key_data: Option<SecretString>,
259
260 #[serde(rename = "as")]
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub impersonate: Option<String>,
264 #[serde(rename = "as-uid")]
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub impersonate_uid: Option<String>,
268 #[serde(rename = "as-groups")]
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub impersonate_groups: Option<Vec<String>>,
272 #[serde(rename = "as-user-extra")]
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub impersonate_user_extra: Option<HashMap<String, Vec<String>>>,
276
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub extensions: Option<Vec<NamedExtension>>,
280
281 #[serde(rename = "auth-provider")]
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub auth_provider: Option<AuthProviderConfig>,
285
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub exec: Option<ExecConfig>,
289
290 #[serde(flatten)]
295 pub other: BTreeMap<String, serde_json::Value>,
296}
297
298#[cfg(test)]
299impl PartialEq for AuthInfo {
300 fn eq(&self, other: &Self) -> bool {
301 serde_json::to_value(self).unwrap() == serde_json::to_value(other).unwrap()
302 }
303}
304
305#[derive(Clone, Debug, Serialize, Deserialize, Default)]
307#[cfg_attr(test, derive(PartialEq, Eq))]
308pub struct AuthProviderConfig {
309 pub name: String,
311 #[serde(default)]
313 pub config: HashMap<String, String>,
314
315 #[serde(flatten)]
320 pub other: BTreeMap<String, serde_json::Value>,
321}
322
323#[derive(Clone, Debug, Serialize, Deserialize, Default)]
325#[cfg_attr(test, derive(PartialEq, Eq))]
326pub struct ExecConfig {
327 #[serde(rename = "apiVersion")]
331 #[serde(skip_serializing_if = "Option::is_none")]
332 pub api_version: Option<String>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub command: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub args: Option<Vec<String>>,
339 #[serde(skip_serializing_if = "Option::is_none")]
343 pub env: Option<Vec<HashMap<String, String>>>,
344 #[serde(skip)]
349 pub drop_env: Option<Vec<String>>,
350
351 #[serde(rename = "installHint")]
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub install_hint: Option<String>,
356
357 #[serde(rename = "interactiveMode")]
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub interactive_mode: Option<ExecInteractiveMode>,
361
362 #[serde(default, rename = "provideClusterInfo")]
368 pub provide_cluster_info: bool,
369
370 #[serde(skip)]
373 pub cluster: Option<ExecAuthCluster>,
374
375 #[serde(flatten)]
380 pub other: BTreeMap<String, serde_json::Value>,
381}
382
383#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
385#[cfg_attr(test, derive(Eq))]
386pub enum ExecInteractiveMode {
387 Never,
389 IfAvailable,
391 Always,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize, Default)]
397#[cfg_attr(test, derive(PartialEq, Eq))]
398pub struct NamedContext {
399 pub name: String,
401 #[serde(skip_serializing_if = "Option::is_none")]
403 pub context: Option<Context>,
404
405 #[serde(flatten)]
410 pub other: BTreeMap<String, serde_json::Value>,
411}
412
413#[derive(Clone, Debug, Serialize, Deserialize, Default)]
415#[cfg_attr(test, derive(PartialEq, Eq))]
416pub struct Context {
417 pub cluster: String,
419 pub user: Option<String>,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub namespace: Option<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub extensions: Option<Vec<NamedExtension>>,
427
428 #[serde(flatten)]
433 pub other: BTreeMap<String, serde_json::Value>,
434}
435
436const KUBECONFIG: &str = "KUBECONFIG";
437
438impl Kubeconfig {
440 pub fn read_from<P: AsRef<Path>>(path: P) -> Result<Kubeconfig, KubeconfigError> {
442 let data =
443 read_path(&path).map_err(|source| KubeconfigError::ReadConfig(source, path.as_ref().into()))?;
444
445 let mut merged_docs = None;
447 for mut config in kubeconfig_from_yaml(&data)? {
448 if let Some(dir) = path.as_ref().parent() {
449 for named in config.clusters.iter_mut() {
450 if let Some(cluster) = &mut named.cluster
451 && let Some(path) = &cluster.certificate_authority
452 && let Some(abs_path) = to_absolute(dir, path)
453 {
454 cluster.certificate_authority = Some(abs_path);
455 }
456 }
457 for named in config.auth_infos.iter_mut() {
458 if let Some(auth_info) = &mut named.auth_info {
459 if let Some(path) = &auth_info.client_certificate
460 && let Some(abs_path) = to_absolute(dir, path)
461 {
462 auth_info.client_certificate = Some(abs_path);
463 }
464 if let Some(path) = &auth_info.client_key
465 && let Some(abs_path) = to_absolute(dir, path)
466 {
467 auth_info.client_key = Some(abs_path);
468 }
469 if let Some(path) = &auth_info.token_file
470 && let Some(abs_path) = to_absolute(dir, path)
471 {
472 auth_info.token_file = Some(abs_path);
473 }
474 if let Some(exec) = &mut auth_info.exec
479 && let Some(command) = &exec.command
480 && command.contains(std::path::MAIN_SEPARATOR)
481 && let Some(abs_path) = to_absolute(dir, command)
482 {
483 exec.command = Some(abs_path);
484 }
485 }
486 }
487 }
488 if let Some(c) = merged_docs {
489 merged_docs = Some(Kubeconfig::merge(c, config)?);
490 } else {
491 merged_docs = Some(config);
492 }
493 }
494 Ok(merged_docs.unwrap_or_default())
496 }
497
498 pub fn from_yaml(text: &str) -> Result<Kubeconfig, KubeconfigError> {
503 kubeconfig_from_yaml(text)?
504 .into_iter()
505 .try_fold(Kubeconfig::default(), Kubeconfig::merge)
506 }
507
508 pub fn read() -> Result<Kubeconfig, KubeconfigError> {
510 match Self::from_env()? {
511 Some(config) => Ok(config),
512 None => Self::read_from(default_kube_path().ok_or(KubeconfigError::FindPath)?),
513 }
514 }
515
516 pub fn from_env() -> Result<Option<Self>, KubeconfigError> {
523 match std::env::var_os(KUBECONFIG) {
524 Some(value) => {
525 let paths = std::env::split_paths(&value)
526 .filter(|p| !p.as_os_str().is_empty())
527 .collect::<Vec<_>>();
528 if paths.is_empty() {
529 return Ok(None);
530 }
531
532 let merged = paths.iter().try_fold(Kubeconfig::default(), |m, p| {
533 Kubeconfig::read_from(p).and_then(|c| m.merge(c))
534 })?;
535 Ok(Some(merged))
536 }
537
538 None => Ok(None),
539 }
540 }
541
542 pub fn merge(mut self, next: Kubeconfig) -> Result<Self, KubeconfigError> {
555 if self.kind.is_some() && next.kind.is_some() && self.kind != next.kind {
556 return Err(KubeconfigError::KindMismatch);
557 }
558 if self.api_version.is_some() && next.api_version.is_some() && self.api_version != next.api_version {
559 return Err(KubeconfigError::ApiVersionMismatch);
560 }
561
562 self.kind = self.kind.or(next.kind);
563 self.api_version = self.api_version.or(next.api_version);
564 self.preferences = self.preferences.or(next.preferences);
565 append_new_named(&mut self.clusters, next.clusters, |x| &x.name);
566 append_new_named(&mut self.auth_infos, next.auth_infos, |x| &x.name);
567 append_new_named(&mut self.contexts, next.contexts, |x| &x.name);
568 self.current_context = self.current_context.or(next.current_context);
569 self.extensions = self.extensions.or(next.extensions);
570 for (key, value) in next.other {
572 self.other.entry(key).or_insert(value);
573 }
574 Ok(self)
575 }
576}
577
578fn kubeconfig_from_yaml(text: &str) -> Result<Vec<Kubeconfig>, KubeconfigError> {
579 serde_saphyr::from_multiple(text).map_err(|e| KubeconfigError::Parse(Box::new(e)))
580}
581
582fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
583where
584 F: Fn(&T) -> &String,
585{
586 use std::collections::HashSet;
587 base.extend({
588 let existing = base.iter().map(&f).collect::<HashSet<_>>();
589 next.into_iter()
590 .filter(|x| !existing.contains(f(x)))
591 .collect::<Vec<_>>()
592 });
593}
594
595fn read_path<P: AsRef<Path>>(path: P) -> io::Result<String> {
596 let bytes = fs::read(&path)?;
597 match bytes.as_slice() {
598 [0xFF, 0xFE, ..] => {
599 let utf16_data: Vec<u16> = bytes[2..]
600 .chunks(2)
601 .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
602 .collect();
603 String::from_utf16(&utf16_data)
604 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 LE"))
605 }
606 [0xFE, 0xFF, ..] => {
607 let utf16_data: Vec<u16> = bytes[2..]
608 .chunks(2)
609 .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
610 .collect();
611 String::from_utf16(&utf16_data)
612 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16 BE"))
613 }
614 [0xEF, 0xBB, 0xBF, ..] => String::from_utf8(bytes[3..].to_vec())
615 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8 BOM")),
616 _ => {
617 String::from_utf8(bytes).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-8"))
618 }
619 }
620}
621
622fn to_absolute(dir: &Path, file: &str) -> Option<String> {
623 let path = Path::new(&file);
624 if path.is_relative() {
625 dir.join(path).to_str().map(str::to_owned)
626 } else {
627 None
628 }
629}
630
631impl Cluster {
632 pub(crate) fn load_certificate_authority(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
633 if self.certificate_authority.is_none() && self.certificate_authority_data.is_none() {
634 return Ok(None);
635 }
636
637 let ca = load_from_base64_or_file(
638 &self.certificate_authority_data.as_deref(),
639 &self.certificate_authority,
640 )
641 .map_err(KubeconfigError::LoadCertificateAuthority)?;
642 Ok(ca)
643 }
644}
645
646impl AuthInfo {
647 pub(crate) fn identity_pem(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
648 let client_cert = self.load_client_certificate()?;
649 let client_key = self.load_client_key()?;
650
651 match (client_cert, client_key) {
652 (None, None) => Ok(None),
653
654 (Some(_), None) => Err(KubeconfigError::LoadClientKey(
655 LoadDataError::NoBase64DataOrFile,
656 )),
657
658 (None, Some(_)) => Err(KubeconfigError::LoadClientCertificate(
659 LoadDataError::NoBase64DataOrFile,
660 )),
661
662 (Some(cert), Some(mut key)) => {
663 key.extend_from_slice(&cert);
664 Ok(Some(key))
665 },
666 }
667 }
668
669 pub(crate) fn load_client_certificate(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
670 load_from_base64_or_file(&self.client_certificate_data.as_deref(), &self.client_certificate)
673 .map_err(KubeconfigError::LoadClientCertificate)
674 }
675
676 pub(crate) fn load_client_key(&self) -> Result<Option<Vec<u8>>, KubeconfigError> {
677 load_from_base64_or_file(
680 &self.client_key_data.as_ref().map(|secret| secret.expose_secret()),
681 &self.client_key,
682 )
683 .map_err(KubeconfigError::LoadClientKey)
684 }
685}
686
687#[derive(Clone, Debug, Serialize, Deserialize, Default)]
692#[serde(rename_all = "kebab-case")]
693#[cfg_attr(test, derive(PartialEq, Eq))]
694pub struct ExecAuthCluster {
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub server: Option<String>,
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub insecure_skip_tls_verify: Option<bool>,
701 #[serde(default, skip_serializing_if = "Option::is_none")]
703 #[serde(with = "base64serde")]
704 pub certificate_authority_data: Option<Vec<u8>>,
705 #[serde(skip_serializing_if = "Option::is_none")]
707 pub proxy_url: Option<String>,
708 #[serde(skip_serializing_if = "Option::is_none")]
712 pub tls_server_name: Option<String>,
713 #[serde(skip_serializing_if = "Option::is_none")]
715 pub config: Option<serde_json::Value>,
716}
717
718impl TryFrom<&Cluster> for ExecAuthCluster {
719 type Error = KubeconfigError;
720
721 fn try_from(cluster: &crate::config::Cluster) -> Result<Self, KubeconfigError> {
722 let certificate_authority_data = cluster.load_certificate_authority()?;
723 Ok(Self {
724 server: cluster.server.clone(),
725 insecure_skip_tls_verify: cluster.insecure_skip_tls_verify,
726 certificate_authority_data,
727 proxy_url: cluster.proxy_url.clone(),
728 tls_server_name: cluster.tls_server_name.clone(),
729 config: cluster.extensions.as_ref().and_then(|extensions| {
730 extensions
731 .iter()
732 .find(|extension| extension.name == CLUSTER_EXTENSION_KEY)
733 .map(|extension| extension.extension.clone())
734 }),
735 })
736 }
737}
738
739fn load_from_base64_or_file<P: AsRef<Path>>(
740 value: &Option<&str>,
741 file: &Option<P>,
742) -> Result<Option<Vec<u8>>, LoadDataError> {
743 let data = value
744 .map(load_from_base64)
745 .or_else(|| file.as_ref().map(load_from_file));
746 match data {
747 Some(data) => Ok(Some(ensure_trailing_newline(data?))),
748 None => Ok(None),
749 }
750}
751
752fn load_from_base64(value: &str) -> Result<Vec<u8>, LoadDataError> {
753 use base64::Engine;
754 base64::engine::general_purpose::STANDARD
755 .decode(value)
756 .map_err(LoadDataError::DecodeBase64)
757}
758
759fn load_from_file<P: AsRef<Path>>(file: &P) -> Result<Vec<u8>, LoadDataError> {
760 fs::read(file).map_err(|source| LoadDataError::ReadFile(source, file.as_ref().into()))
761}
762
763fn ensure_trailing_newline(mut data: Vec<u8>) -> Vec<u8> {
766 if data.last().map(|end| *end != b'\n').unwrap_or(false) {
767 data.push(b'\n');
768 }
769 data
770}
771
772fn default_kube_path() -> Option<PathBuf> {
774 std::env::home_dir().map(|h| h.join(".kube").join("config"))
780}
781
782mod base64serde {
783 use base64::Engine;
784 use serde::{Deserialize, Deserializer, Serialize, Serializer};
785
786 pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
787 match v {
788 Some(v) => {
789 let encoded = base64::engine::general_purpose::STANDARD.encode(v);
790 String::serialize(&encoded, s)
791 }
792 None => <Option<String>>::serialize(&None, s),
793 }
794 }
795
796 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
797 let data = <Option<String>>::deserialize(d)?;
798 match data {
799 Some(data) => Ok(Some(
800 base64::engine::general_purpose::STANDARD
801 .decode(data.as_bytes())
802 .map_err(serde::de::Error::custom)?,
803 )),
804 None => Ok(None),
805 }
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use crate::config::file_loader::ConfigLoader;
812
813 use super::*;
814 use serde_json::{Value, json};
815
816 #[test]
817 fn kubeconfig_merge() {
818 let kubeconfig1 = Kubeconfig {
819 current_context: Some("default".into()),
820 auth_infos: vec![NamedAuthInfo {
821 name: "red-user".into(),
822 auth_info: Some(AuthInfo {
823 token: Some(SecretString::new("first-token".into())),
824 ..Default::default()
825 }),
826 ..Default::default()
827 }],
828 ..Default::default()
829 };
830 let kubeconfig2 = Kubeconfig {
831 current_context: Some("dev".into()),
832 auth_infos: vec![
833 NamedAuthInfo {
834 name: "red-user".into(),
835 auth_info: Some(AuthInfo {
836 token: Some(SecretString::new("second-token".into())),
837 username: Some("red-user".into()),
838 ..Default::default()
839 }),
840 ..Default::default()
841 },
842 NamedAuthInfo {
843 name: "green-user".into(),
844 auth_info: Some(AuthInfo {
845 token: Some(SecretString::new("new-token".into())),
846 ..Default::default()
847 }),
848 ..Default::default()
849 },
850 ],
851 ..Default::default()
852 };
853
854 let merged = kubeconfig1.merge(kubeconfig2).unwrap();
855 assert_eq!(merged.current_context, Some("default".into()));
857 assert_eq!(merged.auth_infos[0].name, "red-user");
859 assert_eq!(
860 merged.auth_infos[0]
861 .auth_info
862 .as_ref()
863 .unwrap()
864 .token
865 .as_ref()
866 .map(|t| t.expose_secret()),
867 Some("first-token")
868 );
869 assert_eq!(merged.auth_infos[0].auth_info.as_ref().unwrap().username, None);
871 assert_eq!(merged.auth_infos[1].name, "green-user");
873 }
874
875 #[test]
876 fn kubeconfig_deserialize() {
877 let config_yaml = "apiVersion: v1
878clusters:
879- cluster:
880 certificate-authority-data: LS0t<SNIP>LS0tLQo=
881 server: https://ABCDEF0123456789.gr7.us-west-2.eks.amazonaws.com
882 name: eks
883- cluster:
884 certificate-authority: /home/kevin/.minikube/ca.crt
885 extensions:
886 - extension:
887 last-update: Thu, 18 Feb 2021 16:59:26 PST
888 provider: minikube.sigs.k8s.io
889 version: v1.17.1
890 name: cluster_info
891 server: https://192.168.49.2:8443
892 name: minikube
893contexts:
894- context:
895 cluster: minikube
896 extensions:
897 - extension:
898 last-update: Thu, 18 Feb 2021 16:59:26 PST
899 provider: minikube.sigs.k8s.io
900 version: v1.17.1
901 name: context_info
902 namespace: default
903 user: minikube
904 name: minikube
905- context:
906 cluster: arn:aws:eks:us-west-2:012345678912:cluster/eks
907 user: arn:aws:eks:us-west-2:012345678912:cluster/eks
908 name: eks
909current-context: minikube
910kind: Config
911preferences: {}
912users:
913- name: arn:aws:eks:us-west-2:012345678912:cluster/eks
914 user:
915 exec:
916 apiVersion: client.authentication.k8s.io/v1alpha1
917 args:
918 - --region
919 - us-west-2
920 - eks
921 - get-token
922 - --cluster-name
923 - eks
924 command: aws
925 env: null
926 installHint: Please install aws cli
927 provideClusterInfo: false
928 as: admin
929 as-uid: '12345'
930 as-groups:
931 - group1
932 - group2
933 as-user-extra:
934 scopes:
935 - read
936 - write
937 extensions:
938 - name: authinfo_ext
939 extension:
940 key: value
941- name: minikube
942 user:
943 client-certificate: /home/kevin/.minikube/profiles/minikube/client.crt
944 client-key: /home/kevin/.minikube/profiles/minikube/client.key";
945
946 let config = Kubeconfig::from_yaml(config_yaml).unwrap();
947
948 assert_eq!(config.clusters[0].name, "eks");
949 assert_eq!(config.clusters[1].name, "minikube");
950
951 let cluster1 = config.clusters[1].cluster.as_ref().unwrap();
952 assert_eq!(
953 cluster1.extensions.as_ref().unwrap()[0].extension.get("provider"),
954 Some(&Value::String("minikube.sigs.k8s.io".to_owned()))
955 );
956
957 let auth_info = config.auth_infos[0].auth_info.as_ref().unwrap();
959 assert_eq!(auth_info.impersonate.as_deref(), Some("admin"));
960 assert_eq!(auth_info.impersonate_uid.as_deref(), Some("12345"));
961 assert_eq!(
962 auth_info.impersonate_groups.as_deref(),
963 Some(["group1".to_string(), "group2".to_string()].as_slice())
964 );
965 let extra = auth_info.impersonate_user_extra.as_ref().unwrap();
966 assert_eq!(
967 extra.get("scopes").unwrap(),
968 &vec!["read".to_string(), "write".to_string()]
969 );
970 let auth_ext = auth_info.extensions.as_ref().unwrap();
971 assert_eq!(auth_ext[0].name, "authinfo_ext");
972
973 let exec = auth_info.exec.as_ref().unwrap();
975 assert_eq!(exec.install_hint.as_deref(), Some("Please install aws cli"));
976 }
977
978 #[test]
979 fn kubeconfig_multi_document_merge() -> Result<(), KubeconfigError> {
980 let config_yaml = r#"---
981apiVersion: v1
982clusters:
983- cluster:
984 certificate-authority-data: aGVsbG8K
985 server: https://0.0.0.0:6443
986 name: k3d-promstack
987contexts:
988- context:
989 cluster: k3d-promstack
990 user: admin@k3d-promstack
991 name: k3d-promstack
992current-context: k3d-promstack
993kind: Config
994preferences: {}
995users:
996- name: admin@k3d-promstack
997 user:
998 client-certificate-data: aGVsbG8K
999 client-key-data: aGVsbG8K
1000---
1001apiVersion: v1
1002clusters:
1003- cluster:
1004 certificate-authority-data: aGVsbG8K
1005 server: https://0.0.0.0:6443
1006 name: k3d-k3s-default
1007contexts:
1008- context:
1009 cluster: k3d-k3s-default
1010 user: admin@k3d-k3s-default
1011 name: k3d-k3s-default
1012current-context: k3d-k3s-default
1013kind: Config
1014preferences: {}
1015users:
1016- name: admin@k3d-k3s-default
1017 user:
1018 client-certificate-data: aGVsbG8K
1019 client-key-data: aGVsbG8K
1020"#;
1021 let cfg = Kubeconfig::from_yaml(config_yaml)?;
1022
1023 assert_eq!(cfg.clusters[0].name, "k3d-promstack");
1025 assert_eq!(cfg.clusters[1].name, "k3d-k3s-default");
1026
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn kubeconfig_split_sections_merge() -> Result<(), KubeconfigError> {
1032 let config1 = r#"
1033apiVersion: v1
1034clusters:
1035- cluster:
1036 certificate-authority-data: aGVsbG8K
1037 server: https://0.0.0.0:6443
1038 name: k3d-promstack
1039contexts:
1040- context:
1041 cluster: k3d-promstack
1042 user: admin@k3d-promstack
1043 name: k3d-promstack
1044current-context: k3d-promstack
1045kind: Config
1046preferences: {}
1047"#;
1048
1049 let config2 = r#"
1050users:
1051- name: admin@k3d-k3s-default
1052 user:
1053 client-certificate-data: aGVsbG8K
1054 client-key-data: aGVsbG8K
1055"#;
1056
1057 let kubeconfig1 = Kubeconfig::from_yaml(config1)?;
1058 let kubeconfig2 = Kubeconfig::from_yaml(config2)?;
1059 let merged = kubeconfig1.merge(kubeconfig2).unwrap();
1060
1061 assert_eq!(merged.clusters[0].name, "k3d-promstack");
1063 assert_eq!(merged.contexts[0].name, "k3d-promstack");
1064 assert_eq!(merged.auth_infos[0].name, "admin@k3d-k3s-default");
1065
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn kubeconfig_from_empty_string() {
1071 let cfg = Kubeconfig::from_yaml("").unwrap();
1072
1073 assert_eq!(cfg, Kubeconfig::default());
1074 }
1075
1076 #[test]
1077 fn authinfo_deserialize_null_secret() {
1078 let authinfo_yaml = r#"
1079username: user
1080password:
1081"#;
1082 let authinfo: AuthInfo = serde_saphyr::from_str(authinfo_yaml).unwrap();
1083 assert_eq!(authinfo.username, Some("user".to_string()));
1084 assert!(authinfo.password.is_none());
1085 }
1086
1087 #[test]
1088 fn authinfo_debug_does_not_output_password() {
1089 let authinfo_yaml = r#"
1090username: user
1091password: kube_rs
1092"#;
1093 let authinfo: AuthInfo = serde_saphyr::from_str(authinfo_yaml).unwrap();
1094 let authinfo_debug_output = format!("{authinfo:?}");
1095 let expected_output = "AuthInfo { \
1096 username: Some(\"user\"), \
1097 password: Some(SecretBox<str>([REDACTED])), \
1098 token: None, token_file: None, client_certificate: None, \
1099 client_certificate_data: None, client_key: None, \
1100 client_key_data: None, impersonate: None, \
1101 impersonate_uid: None, \
1102 impersonate_groups: None, \
1103 impersonate_user_extra: None, \
1104 extensions: None, \
1105 auth_provider: None, \
1106 exec: None, \
1107 other: {} \
1108 }";
1109
1110 assert_eq!(authinfo_debug_output, expected_output)
1111 }
1112
1113 #[tokio::test]
1114 async fn authinfo_exec_provide_cluster_info() {
1115 let config = r#"
1116apiVersion: v1
1117clusters:
1118- cluster:
1119 server: https://localhost:8080
1120 extensions:
1121 - name: client.authentication.k8s.io/exec
1122 extension:
1123 audience: foo
1124 other: bar
1125 name: foo-cluster
1126contexts:
1127- context:
1128 cluster: foo-cluster
1129 user: foo-user
1130 namespace: bar
1131 name: foo-context
1132current-context: foo-context
1133kind: Config
1134users:
1135- name: foo-user
1136 user:
1137 exec:
1138 apiVersion: client.authentication.k8s.io/v1alpha1
1139 args:
1140 - arg-1
1141 - arg-2
1142 command: foo-command
1143 provideClusterInfo: true
1144"#;
1145 let kube_config = Kubeconfig::from_yaml(config).unwrap();
1146 let config_loader = ConfigLoader::load(kube_config, None, None, None).await.unwrap();
1147 let auth_info = config_loader.user;
1148 let exec = auth_info.exec.unwrap();
1149 assert!(exec.provide_cluster_info);
1150 let cluster = exec.cluster.unwrap();
1151 assert_eq!(
1152 cluster.config.unwrap(),
1153 json!({"audience": "foo", "other": "bar"})
1154 );
1155 }
1156
1157 #[tokio::test]
1158 async fn parse_kubeconfig_encodings() {
1159 let files = vec![
1160 "kubeconfig_utf8.yaml",
1161 "kubeconfig_utf16le.yaml",
1162 "kubeconfig_utf16be.yaml",
1163 ];
1164
1165 for file_name in files {
1166 let path = PathBuf::from(format!(
1167 "{}/src/config/test_data/{}",
1168 env!("CARGO_MANIFEST_DIR"),
1169 file_name
1170 ));
1171 let cfg = Kubeconfig::read_from(path).unwrap();
1172 assert_eq!(cfg.clusters[0].name, "k3d-promstack");
1173 assert_eq!(cfg.contexts[0].name, "k3d-promstack");
1174 assert_eq!(cfg.auth_infos[0].name, "admin@k3d-k3s-default");
1175 }
1176 }
1177
1178 #[test]
1179 fn read_from_resolves_relative_exec_command() {
1180 let dir = tempfile::TempDir::new().unwrap();
1184 let path = dir.path().join("config");
1185 let rel_cmd = format!("auth{}token.sh", std::path::MAIN_SEPARATOR);
1189 std::fs::write(
1190 &path,
1191 format!(
1192 r#"
1193apiVersion: v1
1194kind: Config
1195current-context: ctx
1196clusters:
1197- name: cluster
1198 cluster:
1199 server: https://localhost:6443
1200contexts:
1201- name: ctx
1202 context:
1203 cluster: cluster
1204 user: relative-user
1205users:
1206- name: relative-user
1207 user:
1208 exec:
1209 apiVersion: client.authentication.k8s.io/v1beta1
1210 command: {rel_cmd}
1211- name: path-user
1212 user:
1213 exec:
1214 apiVersion: client.authentication.k8s.io/v1beta1
1215 command: aws
1216"#
1217 ),
1218 )
1219 .unwrap();
1220
1221 let cfg = Kubeconfig::read_from(&path).unwrap();
1222
1223 let expected = to_absolute(dir.path(), &rel_cmd);
1225 assert_eq!(
1226 cfg.auth_infos[0]
1227 .auth_info
1228 .as_ref()
1229 .and_then(|a| a.exec.as_ref())
1230 .and_then(|e| e.command.clone()),
1231 expected
1232 );
1233
1234 assert_eq!(
1236 cfg.auth_infos[1]
1237 .auth_info
1238 .as_ref()
1239 .and_then(|a| a.exec.as_ref())
1240 .and_then(|e| e.command.as_deref()),
1241 Some("aws")
1242 );
1243 }
1244
1245 #[test]
1246 fn kubeconfig_round_trip_preserves_unknown_fields() {
1247 let yaml = r#"
1248apiVersion: v1
1249kind: Config
1250current-context: test
1251custom-top-level-field: should-be-preserved
1252clusters:
1253- name: test-cluster
1254 cluster:
1255 server: https://localhost:6443
1256 certificate-authority-data: dGVzdA==
1257 custom-cluster-field: cluster-extra
1258contexts:
1259- name: test
1260 context:
1261 cluster: test-cluster
1262 user: test-user
1263 custom-context-field: context-extra
1264users:
1265- name: test-user
1266 user:
1267 exec:
1268 apiVersion: client.authentication.k8s.io/v1beta1
1269 command: gke-gcloud-auth-plugin
1270 provideClusterInfo: true
1271 interactiveMode: IfAvailable
1272 custom-exec-field: exec-extra
1273"#;
1274
1275 let config: Kubeconfig = Kubeconfig::from_yaml(yaml).unwrap();
1276
1277 let exec = config.auth_infos[0]
1279 .auth_info
1280 .as_ref()
1281 .unwrap()
1282 .exec
1283 .as_ref()
1284 .unwrap();
1285 assert_eq!(
1286 exec.other.get("custom-exec-field").and_then(|v| v.as_str()),
1287 Some("exec-extra")
1288 );
1289
1290 let serialized = serde_saphyr::to_string(&config).unwrap();
1292
1293 assert!(
1295 serialized.contains("custom-top-level-field"),
1296 "top-level unknown field was lost:\n{serialized}"
1297 );
1298 assert!(
1299 serialized.contains("custom-cluster-field"),
1300 "cluster unknown field was lost:\n{serialized}"
1301 );
1302 assert!(
1303 serialized.contains("custom-context-field"),
1304 "context unknown field was lost:\n{serialized}"
1305 );
1306 assert!(
1307 serialized.contains("custom-exec-field"),
1308 "exec unknown field was lost:\n{serialized}"
1309 );
1310
1311 let reparsed = Kubeconfig::from_yaml(&serialized).unwrap();
1313 assert_eq!(config, reparsed);
1314 }
1315}