diff --git a/Cargo.lock b/Cargo.lock index ef76fa32..ad06ec43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -997,7 +997,7 @@ dependencies = [ [[package]] name = "k8-config" -version = "2.1.0" +version = "2.2.0" dependencies = [ "dirs", "fluvio-future", diff --git a/src/k8-config/Cargo.toml b/src/k8-config/Cargo.toml index 1fede48b..66188604 100644 --- a/src/k8-config/Cargo.toml +++ b/src/k8-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "k8-config" -version = "2.1.0" +version = "2.2.0" authors = ["Fluvio Contributors "] edition = "2021" description = "Read Kubernetes config" @@ -25,4 +25,4 @@ fluvio-future = { workspace = true, features = ["subscriber"]} [[example]] -name = "kubeconfig_read" \ No newline at end of file +name = "kubeconfig_read" diff --git a/src/k8-config/src/config.rs b/src/k8-config/src/config.rs index 500ffd53..f88f78aa 100644 --- a/src/k8-config/src/config.rs +++ b/src/k8-config/src/config.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] pub certificate_authority: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub certificate_authority_data: Option, pub server: String, } @@ -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, } @@ -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, @@ -64,7 +70,7 @@ pub struct User { // pub config: HashMap, //} -#[derive(Debug, Eq, PartialEq, Deserialize)] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(tag = "name", content = "config")] pub enum AuthProviderDetail { #[serde(alias = "gcp")] @@ -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, pub cmd_args: String, pub cmd_path: String, + #[serde(skip_serializing_if = "Option::is_none")] pub expiry: Option, pub expiry_key: String, pub token_key: String, @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] pub client_certificate: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub client_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub client_certificate_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub client_key_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub exec: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, } -#[derive(Debug, Eq, PartialEq, Deserialize)] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Exec { #[serde(rename = "apiVersion")] @@ -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, @@ -166,8 +185,19 @@ impl KubeConfig { } pub fn from_file>(path: T) -> Result { - 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>(&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> { @@ -191,11 +221,46 @@ impl KubeConfig { None } } + + pub fn put_user(&mut self, user: User) -> Option { + 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 { + 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 { + 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] @@ -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"); + } }