Skip to content

Commit

Permalink
feat: added serialization for k8s config (#266)
Browse files Browse the repository at this point in the history
Added serialization to `K8Config` and an ability to save changes to the original file.

This is needed for adding the embedded k8s context to the kube context file.
  • Loading branch information
Alexander Galibey committed Sep 13, 2023
1 parent 379297b commit 577ad7e
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/k8-config/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "k8-config"
version = "2.1.0"
version = "2.2.0"
authors = ["Fluvio Contributors <team@fluvio.io>"]
edition = "2021"
description = "Read Kubernetes config"
Expand All @@ -25,4 +25,4 @@ fluvio-future = { workspace = true, features = ["subscriber"]}


[[example]]
name = "kubeconfig_read"
name = "kubeconfig_read"
232 changes: 220 additions & 12 deletions src/k8-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ use std::fs::read_to_string;
use std::fs::File;
use std::io::Result as IoResult;
use std::path::Path;
use std::path::PathBuf;

use dirs::home_dir;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;

use crate::ConfigError;

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Cluster {
pub name: String,
pub cluster: ClusterDetail,
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ClusterDetail {
#[serde(skip_serializing_if = "Option::is_none")]
pub insecure_skip_tls_verify: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_authority: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_authority_data: Option<String>,
pub server: String,
}
Expand All @@ -30,16 +35,17 @@ impl ClusterDetail {
}
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Context {
pub name: String,
pub context: ContextDetail,
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct ContextDetail {
pub cluster: String,
pub user: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}

Expand All @@ -52,7 +58,7 @@ impl ContextDetail {
}
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct User {
pub name: String,
pub user: UserDetail,
Expand All @@ -64,7 +70,7 @@ pub struct User {
// pub config: HashMap<String, String>,
//}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "name", content = "config")]
pub enum AuthProviderDetail {
#[serde(alias = "gcp")]
Expand Down Expand Up @@ -101,12 +107,14 @@ impl AuthProviderDetail {
}
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct GcpAuthProviderConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub access_token: Option<String>,
pub cmd_args: String,
pub cmd_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiry: Option<String>,
pub expiry_key: String,
pub token_key: String,
Expand All @@ -123,21 +131,30 @@ pub struct GcpAuthProviderConfig {
// refresh_token: String,
//}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct UserDetail {
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_provider: Option<AuthProviderDetail>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_certificate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_certificate_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_key_data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exec: Option<Exec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Exec {
#[serde(rename = "apiVersion")]
Expand All @@ -146,9 +163,11 @@ pub struct Exec {
pub command: String,
}

#[derive(Debug, Eq, PartialEq, Deserialize)]
#[derive(Debug, Eq, PartialEq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct KubeConfig {
#[serde(skip)]
pub path: PathBuf,
#[serde(rename = "apiVersion")]
pub api_version: String,
pub clusters: Vec<Cluster>,
Expand All @@ -166,8 +185,19 @@ impl KubeConfig {
}

pub fn from_file<T: AsRef<Path>>(path: T) -> Result<Self, ConfigError> {
let file = File::open(path)?;
Ok(serde_yaml::from_reader(file)?)
let file = File::open(path.as_ref())?;
let mut config: Self = serde_yaml::from_reader(file)?;
config.path = path.as_ref().to_path_buf();
Ok(config)
}

pub fn to_file<T: AsRef<Path>>(&self, path: T) -> Result<(), ConfigError> {
let file = File::create(path)?;
Ok(serde_yaml::to_writer(file, self)?)
}

pub fn save(&self) -> Result<(), ConfigError> {
self.to_file(&self.path)
}

pub fn current_context(&self) -> Option<&Context> {
Expand All @@ -191,11 +221,46 @@ impl KubeConfig {
None
}
}

pub fn put_user(&mut self, user: User) -> Option<User> {
let prev = self.users.iter_mut().find(|u| u.name.eq(&user.name));
match prev {
Some(prev) => Some(std::mem::replace(prev, user)),
None => {
self.users.push(user);
None
}
}
}

pub fn put_cluster(&mut self, cluster: Cluster) -> Option<Cluster> {
let prev = self.clusters.iter_mut().find(|c| c.name.eq(&cluster.name));
match prev {
Some(prev) => Some(std::mem::replace(prev, cluster)),
None => {
self.clusters.push(cluster);
None
}
}
}

pub fn put_context(&mut self, context: Context) -> Option<Context> {
let prev = self.contexts.iter_mut().find(|c| c.name.eq(&context.name));
match prev {
Some(prev) => Some(std::mem::replace(prev, context)),
None => {
self.contexts.push(context);
None
}
}
}
}

#[cfg(test)]
mod test {

use crate::{User, Cluster, Context};

use super::KubeConfig;

#[test]
Expand All @@ -219,4 +284,147 @@ mod test {
let current_cluster = config.current_cluster().expect("current");
assert_eq!(current_cluster.name, "minikube");
}

#[test]
fn test_config_ser() {
//given
let config = KubeConfig::from_file("data/k8config.yaml").expect("read");

//when
let serialized = serde_yaml::to_string(&config).expect("serialized");

//then
assert_eq!(
serialized,
r#"apiVersion: v1
clusters:
- name: minikube
cluster:
certificate-authority: /Users/test/.minikube/ca.crt
server: https://192.168.0.0:8443
contexts:
- name: flv
context:
cluster: minikube
user: minikube
namespace: flv
- name: minikube
context:
cluster: minikube
user: minikube
current-context: flv
kind: Config
users:
- name: minikube
user:
client-certificate: /Users/test/.minikube/client.crt
client-key: /Users/test/.minikube/client.key
"#
);
}

#[test]
fn test_put_user() {
//given
let mut config = KubeConfig::default();

let user1 = User {
name: "user1".to_string(),
user: crate::UserDetail {
username: Some("username1".to_string()),
..Default::default()
},
};

let user1_2 = User {
name: "user1".to_string(),
user: crate::UserDetail {
username: Some("username2".to_string()),
..Default::default()
},
};

let user2 = User {
name: "user2".to_string(),
user: Default::default(),
};

//when
assert!(config.put_user(user1).is_none());
assert!(config.put_user(user2).is_none());

let prev = config.put_user(user1_2);
assert!(prev.is_some());
assert_eq!(prev.unwrap().user.username.unwrap(), "username1");
}

#[test]
fn test_put_cluster() {
//given
let mut config = KubeConfig::default();

let cluster1 = Cluster {
name: "cluster1".to_string(),
cluster: crate::ClusterDetail {
server: "server1".to_string(),
..Default::default()
},
};

let cluster1_2 = Cluster {
name: "cluster1".to_string(),
cluster: crate::ClusterDetail {
server: "server2".to_string(),
..Default::default()
},
};

let cluster2 = Cluster {
name: "cluster2".to_string(),
cluster: Default::default(),
};

//when
assert!(config.put_cluster(cluster1).is_none());
assert!(config.put_cluster(cluster2).is_none());

let prev = config.put_cluster(cluster1_2);
assert!(prev.is_some());
assert_eq!(prev.unwrap().cluster.server, "server1");
}

#[test]
fn test_put_context() {
//given
let mut config = KubeConfig::default();

let context1 = Context {
name: "context1".to_string(),
context: crate::ContextDetail {
cluster: "cluster1".to_string(),
..Default::default()
},
};

let context1_2 = Context {
name: "context1".to_string(),
context: crate::ContextDetail {
cluster: "cluster2".to_string(),
..Default::default()
},
};

let context2 = Context {
name: "context2".to_string(),
context: Default::default(),
};

//when
assert!(config.put_context(context1).is_none());
assert!(config.put_context(context2).is_none());

let prev = config.put_context(context1_2);
assert!(prev.is_some());
assert_eq!(prev.unwrap().context.cluster, "cluster1");
}
}

0 comments on commit 577ad7e

Please sign in to comment.