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