Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement Kubeconfig::from_yaml #719

Merged
merged 6 commits into from
Nov 20, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions kube-client/src/config/file_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use super::{KubeconfigError, LoadDataError};
/// [`Config`][crate::Config] is the __intended__ developer interface to help create a [`Client`][crate::Client],
/// and this will handle the difference between in-cluster deployment and local development.
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Kubeconfig {
/// General information to be use for cli interactions
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -48,6 +49,7 @@ pub struct Kubeconfig {

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

/// NamedExtention associates name with extension.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct NamedExtension {
pub name: String,
pub extension: serde_json::Value,
}

/// NamedCluster associates name with cluster.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct NamedCluster {
pub name: String,
pub cluster: Cluster,
}

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

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

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

/// AuthProviderConfig stores auth for specified cloud provider.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct AuthProviderConfig {
pub name: String,
pub config: HashMap<String, String>,
}

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

/// NamedContext associates name with context.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct NamedContext {
pub name: String,
pub context: Context,
}

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

// Remap all files we read to absolute paths.
let mut merged_docs = None;
for mut config in documents {
for mut config in kubeconfig_from_yaml(&data)? {
if let Some(dir) = path.as_ref().parent() {
for named in config.clusters.iter_mut() {
if let Some(path) = &named.cluster.certificate_authority {
Expand Down Expand Up @@ -262,6 +266,16 @@ impl Kubeconfig {
Ok(merged_docs.unwrap_or_default())
}

/// Read a Config from an arbitrary YAML string
///
/// This is preferable to using serde_yaml::from_str() because it will correctly
/// parse multi-document YAML text and merge them into a single `Kubeconfig`
pub fn from_yaml(text: &str) -> Result<Kubeconfig, KubeconfigError> {
kubeconfig_from_yaml(text)?
.into_iter()
.try_fold(Kubeconfig::default(), Kubeconfig::merge)
}

/// Read a Config from `KUBECONFIG` or the the default location.
pub fn read() -> Result<Kubeconfig, KubeconfigError> {
match Self::from_env()? {
Expand Down Expand Up @@ -328,6 +342,16 @@ impl Kubeconfig {
}
}

fn kubeconfig_from_yaml(text: &str) -> Result<Vec<Kubeconfig>, KubeconfigError> {
let mut documents = vec![];
for doc in serde_yaml::Deserializer::from_str(text) {
let value = serde_yaml::Value::deserialize(doc).map_err(KubeconfigError::Parse)?;
let kubeconfig = serde_yaml::from_value(value).map_err(KubeconfigError::InvalidStructure)?;
documents.push(kubeconfig);
}
Ok(documents)
}

#[allow(clippy::redundant_closure)]
fn append_new_named<T, F>(base: &mut Vec<T>, next: Vec<T>, f: F)
where
Expand Down Expand Up @@ -519,7 +543,7 @@ users:
client-certificate: /home/kevin/.minikube/profiles/minikube/client.crt
client-key: /home/kevin/.minikube/profiles/minikube/client.key";

let config: Kubeconfig = serde_yaml::from_str(config_yaml).unwrap();
let config = Kubeconfig::from_yaml(config_yaml).unwrap();

assert_eq!(config.clusters[0].name, "eks");
assert_eq!(config.clusters[1].name, "minikube");
Expand Down Expand Up @@ -574,14 +598,19 @@ users:
client-certificate-data: aGVsbG8K
client-key-data: aGVsbG8K
"#;
let file = tempfile::NamedTempFile::new().expect("create config tempfile");
fs::write(file.path(), config_yaml).unwrap();
let cfg = Kubeconfig::read_from(file.path())?;
let cfg = Kubeconfig::from_yaml(config_yaml)?;

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

Ok(())
}

#[test]
fn kubeconfig_from_empty_string() {
let cfg = Kubeconfig::from_yaml("").unwrap();

assert_eq!(cfg, Kubeconfig::default());
}
}