Skip to content

Commit 16e667f

Browse files
impclux
andauthored
implement Kubeconfig::from_yaml (#719)
* Conditionally derive PartialEq for Kubeconfig and children Signed-off-by: Cyril Plisko <[email protected]> * Add helper for parsing kubeconfig YAML text Signed-off-by: Cyril Plisko <[email protected]> * Add Kubeconfig::from_yaml Fixes #718 Signed-off-by: Cyril Plisko <[email protected]> * Update Kubeconfig::read_from to use YAML helper Signed-off-by: Cyril Plisko <[email protected]> * Update unit tests Signed-off-by: Cyril Plisko <[email protected]> * Update kube-client/src/config/file_config.rs Co-authored-by: Eirik A <[email protected]>
1 parent 9ac2db8 commit 16e667f

File tree

1 file changed

+41
-12
lines changed

1 file changed

+41
-12
lines changed

kube-client/src/config/file_config.rs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use super::{KubeconfigError, LoadDataError};
1919
/// [`Config`][crate::Config] is the __intended__ developer interface to help create a [`Client`][crate::Client],
2020
/// and this will handle the difference between in-cluster deployment and local development.
2121
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
22+
#[cfg_attr(test, derive(PartialEq))]
2223
pub struct Kubeconfig {
2324
/// General information to be use for cli interactions
2425
#[serde(skip_serializing_if = "Option::is_none")]
@@ -48,6 +49,7 @@ pub struct Kubeconfig {
4849

4950
/// Preferences stores extensions for cli.
5051
#[derive(Clone, Debug, Serialize, Deserialize)]
52+
#[cfg_attr(test, derive(PartialEq))]
5153
pub struct Preferences {
5254
#[serde(skip_serializing_if = "Option::is_none")]
5355
pub colors: Option<bool>,
@@ -57,20 +59,23 @@ pub struct Preferences {
5759

5860
/// NamedExtention associates name with extension.
5961
#[derive(Clone, Debug, Serialize, Deserialize)]
62+
#[cfg_attr(test, derive(PartialEq))]
6063
pub struct NamedExtension {
6164
pub name: String,
6265
pub extension: serde_json::Value,
6366
}
6467

6568
/// NamedCluster associates name with cluster.
6669
#[derive(Clone, Debug, Serialize, Deserialize)]
70+
#[cfg_attr(test, derive(PartialEq))]
6771
pub struct NamedCluster {
6872
pub name: String,
6973
pub cluster: Cluster,
7074
}
7175

7276
/// Cluster stores information to connect Kubernetes cluster.
7377
#[derive(Clone, Debug, Serialize, Deserialize)]
78+
#[cfg_attr(test, derive(PartialEq))]
7479
pub struct Cluster {
7580
/// The address of the kubernetes cluster (https://hostname:port).
7681
pub server: String,
@@ -96,6 +101,7 @@ pub struct Cluster {
96101

97102
/// NamedAuthInfo associates name with authentication.
98103
#[derive(Clone, Debug, Serialize, Deserialize)]
104+
#[cfg_attr(test, derive(PartialEq))]
99105
pub struct NamedAuthInfo {
100106
pub name: String,
101107
#[serde(rename = "user")]
@@ -104,6 +110,7 @@ pub struct NamedAuthInfo {
104110

105111
/// AuthInfo stores information to tell cluster who you are.
106112
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
113+
#[cfg_attr(test, derive(PartialEq))]
107114
pub struct AuthInfo {
108115
/// The username for basic authentication to the kubernetes cluster.
109116
#[serde(skip_serializing_if = "Option::is_none")]
@@ -159,13 +166,15 @@ pub struct AuthInfo {
159166

160167
/// AuthProviderConfig stores auth for specified cloud provider.
161168
#[derive(Clone, Debug, Serialize, Deserialize)]
169+
#[cfg_attr(test, derive(PartialEq))]
162170
pub struct AuthProviderConfig {
163171
pub name: String,
164172
pub config: HashMap<String, String>,
165173
}
166174

167175
/// ExecConfig stores credential-plugin configuration.
168176
#[derive(Clone, Debug, Serialize, Deserialize)]
177+
#[cfg_attr(test, derive(PartialEq))]
169178
pub struct ExecConfig {
170179
/// Preferred input version of the ExecInfo.
171180
///
@@ -187,13 +196,15 @@ pub struct ExecConfig {
187196

188197
/// NamedContext associates name with context.
189198
#[derive(Clone, Debug, Serialize, Deserialize)]
199+
#[cfg_attr(test, derive(PartialEq))]
190200
pub struct NamedContext {
191201
pub name: String,
192202
pub context: Context,
193203
}
194204

195205
/// Context stores tuple of cluster and user information.
196206
#[derive(Clone, Debug, Serialize, Deserialize)]
207+
#[cfg_attr(test, derive(PartialEq))]
197208
pub struct Context {
198209
/// Name of the cluster for this context
199210
pub cluster: String,
@@ -215,17 +226,10 @@ impl Kubeconfig {
215226
pub fn read_from<P: AsRef<Path>>(path: P) -> Result<Kubeconfig, KubeconfigError> {
216227
let data = fs::read_to_string(&path)
217228
.map_err(|source| KubeconfigError::ReadConfig(source, path.as_ref().into()))?;
218-
// support multiple documents
219-
let mut documents: Vec<Kubeconfig> = vec![];
220-
for doc in serde_yaml::Deserializer::from_str(&data) {
221-
let value = serde_yaml::Value::deserialize(doc).map_err(KubeconfigError::Parse)?;
222-
let kconf = serde_yaml::from_value(value).map_err(KubeconfigError::InvalidStructure)?;
223-
documents.push(kconf)
224-
}
225229

226230
// Remap all files we read to absolute paths.
227231
let mut merged_docs = None;
228-
for mut config in documents {
232+
for mut config in kubeconfig_from_yaml(&data)? {
229233
if let Some(dir) = path.as_ref().parent() {
230234
for named in config.clusters.iter_mut() {
231235
if let Some(path) = &named.cluster.certificate_authority {
@@ -262,6 +266,16 @@ impl Kubeconfig {
262266
Ok(merged_docs.unwrap_or_default())
263267
}
264268

269+
/// Read a Config from an arbitrary YAML string
270+
///
271+
/// This is preferable to using serde_yaml::from_str() because it will correctly
272+
/// parse multi-document YAML text and merge them into a single `Kubeconfig`
273+
pub fn from_yaml(text: &str) -> Result<Kubeconfig, KubeconfigError> {
274+
kubeconfig_from_yaml(text)?
275+
.into_iter()
276+
.try_fold(Kubeconfig::default(), Kubeconfig::merge)
277+
}
278+
265279
/// Read a Config from `KUBECONFIG` or the the default location.
266280
pub fn read() -> Result<Kubeconfig, KubeconfigError> {
267281
match Self::from_env()? {
@@ -328,6 +342,16 @@ impl Kubeconfig {
328342
}
329343
}
330344

345+
fn kubeconfig_from_yaml(text: &str) -> Result<Vec<Kubeconfig>, KubeconfigError> {
346+
let mut documents = vec![];
347+
for doc in serde_yaml::Deserializer::from_str(text) {
348+
let value = serde_yaml::Value::deserialize(doc).map_err(KubeconfigError::Parse)?;
349+
let kubeconfig = serde_yaml::from_value(value).map_err(KubeconfigError::InvalidStructure)?;
350+
documents.push(kubeconfig);
351+
}
352+
Ok(documents)
353+
}
354+
331355
#[allow(clippy::redundant_closure)]
332356
fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
333357
where
@@ -519,7 +543,7 @@ users:
519543
client-certificate: /home/kevin/.minikube/profiles/minikube/client.crt
520544
client-key: /home/kevin/.minikube/profiles/minikube/client.key";
521545

522-
let config: Kubeconfig = serde_yaml::from_str(config_yaml).unwrap();
546+
let config = Kubeconfig::from_yaml(config_yaml).unwrap();
523547

524548
assert_eq!(config.clusters[0].name, "eks");
525549
assert_eq!(config.clusters[1].name, "minikube");
@@ -574,14 +598,19 @@ users:
574598
client-certificate-data: aGVsbG8K
575599
client-key-data: aGVsbG8K
576600
"#;
577-
let file = tempfile::NamedTempFile::new().expect("create config tempfile");
578-
fs::write(file.path(), config_yaml).unwrap();
579-
let cfg = Kubeconfig::read_from(file.path())?;
601+
let cfg = Kubeconfig::from_yaml(config_yaml)?;
580602

581603
// Ensure we have data from both documents:
582604
assert_eq!(cfg.clusters[0].name, "k3d-promstack");
583605
assert_eq!(cfg.clusters[1].name, "k3d-k3s-default");
584606

585607
Ok(())
586608
}
609+
610+
#[test]
611+
fn kubeconfig_from_empty_string() {
612+
let cfg = Kubeconfig::from_yaml("").unwrap();
613+
614+
assert_eq!(cfg, Kubeconfig::default());
615+
}
587616
}

0 commit comments

Comments
 (0)