use std::path::PathBuf;
use anyhow::{
Context,
Result,
};
use kftray_commons::config_dir::get_kubeconfig_paths;
use kube::config::{
Config,
KubeConfigOptions,
Kubeconfig,
};
use log::{
error,
info,
};
use openssl::base64::{
decode_block,
encode_block,
};
use secrecy::ExposeSecret;
use super::utils::{
convert_pkcs8_to_pkcs1,
is_pkcs8_key,
};
pub trait ConfigExtClone {
fn clone_with_invalid_certs(&self, accept_invalid_certs: bool) -> Self;
}
impl ConfigExtClone for Config {
fn clone_with_invalid_certs(&self, accept_invalid_certs: bool) -> Self {
let mut config = self.clone();
config.accept_invalid_certs = accept_invalid_certs;
config
}
}
pub fn get_kubeconfig_paths_from_option(kubeconfig: Option<String>) -> Result<Vec<PathBuf>> {
match kubeconfig {
Some(path) if path == "default" => {
info!("Using default kubeconfig paths.");
get_kubeconfig_paths()
}
Some(path) => {
info!("Using provided kubeconfig paths: {path}");
Ok(path
.split(if cfg!(windows) { ';' } else { ':' })
.map(PathBuf::from)
.collect())
}
None => {
info!("No kubeconfig path provided, using default paths.");
get_kubeconfig_paths()
}
}
}
pub fn merge_kubeconfigs(paths: &[PathBuf]) -> Result<(Kubeconfig, Vec<String>)> {
let mut errors = Vec::new();
let mut merged_kubeconfig = Kubeconfig::default();
for path in paths {
info!("Attempting to read kubeconfig from path: {path:?}");
match Kubeconfig::read_from(path) {
Ok(kubeconfig) => {
info!("Successfully read kubeconfig from {path:?}");
match merged_kubeconfig.clone().merge(kubeconfig) {
Ok(merged) => merged_kubeconfig = merged,
Err(e) => {
let error_msg = format!("Failed to merge kubeconfig from {path:?}: {e}");
error!("{error_msg}");
errors.push(error_msg);
}
}
}
Err(e) => {
let error_msg = format!("Failed to read kubeconfig from {path:?}: {e}");
error!("{error_msg}");
errors.push(error_msg);
}
}
}
Ok((merged_kubeconfig, errors))
}
pub async fn create_config_with_context(
kubeconfig: &Kubeconfig, context_name: &str,
) -> Result<Config> {
info!("Creating configuration for context: {context_name}");
let mut kubeconfig = kubeconfig.clone();
for auth_info in &mut kubeconfig.auth_infos {
if let Some(ref mut auth_info_data) = auth_info.auth_info
&& let Some(client_key_data) = &auth_info_data.client_key_data
{
let decoded_key = decode_block(client_key_data.expose_secret())
.context("Failed to decode client key data")?;
if is_pkcs8_key(&decoded_key)
&& let Some(converted_key) = convert_pkcs8_to_pkcs1(&decoded_key)
.context("Failed to convert PKCS#8 key to PKCS#1")?
{
let encoded_key = encode_block(&converted_key);
auth_info_data.client_key_data = Some(encoded_key.into());
}
}
}
let context_to_use = if context_name == "@current" {
kubeconfig.current_context.clone()
} else {
Some(context_name.to_owned())
};
Config::from_custom_kubeconfig(
kubeconfig,
&KubeConfigOptions {
context: context_to_use,
..Default::default()
},
)
.await
.context("Failed to create configuration from kubeconfig")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kube::operations::list_contexts;
#[test]
fn test_get_kubeconfig_paths_from_option() {
use std::env;
use std::fs;
use std::sync::Mutex;
use tempfile::TempDir;
lazy_static::lazy_static! {
static ref ENV_LOCK: Mutex<()> = Mutex::new(());
}
let _env_guard = ENV_LOCK.lock().unwrap();
let original_kubeconfig = env::var("KUBECONFIG").ok();
let original_home = env::var("HOME").ok();
let temp_dir = TempDir::new().unwrap();
let separator = if cfg!(windows) { ';' } else { ':' };
let test_paths = if cfg!(windows) {
format!("C:\\path1{separator}D:\\path2")
} else {
format!("/path1{separator}/path2")
};
let explicit_paths = get_kubeconfig_paths_from_option(Some(test_paths.clone())).unwrap();
assert_eq!(explicit_paths.len(), 2);
if cfg!(windows) {
assert_eq!(explicit_paths[0], std::path::Path::new("C:\\path1"));
assert_eq!(explicit_paths[1], std::path::Path::new("D:\\path2"));
} else {
assert_eq!(explicit_paths[0], std::path::Path::new("/path1"));
assert_eq!(explicit_paths[1], std::path::Path::new("/path2"));
}
let mock_kubeconfig_path = temp_dir.path().join("mock_kubeconfig");
fs::write(&mock_kubeconfig_path, "mock kubeconfig content").unwrap();
unsafe { env::set_var("KUBECONFIG", mock_kubeconfig_path.to_str().unwrap()) };
let default_path_result = get_kubeconfig_paths_from_option(Some("default".to_string()));
assert!(default_path_result.is_ok());
let default_path = default_path_result.unwrap();
assert!(!default_path.is_empty());
assert_eq!(default_path[0], mock_kubeconfig_path);
let none_path_result = get_kubeconfig_paths_from_option(None);
assert!(none_path_result.is_ok());
let none_path = none_path_result.unwrap();
assert!(!none_path.is_empty());
assert_eq!(none_path[0], mock_kubeconfig_path);
let fake_home = temp_dir.path().join("fake_home");
fs::create_dir_all(fake_home.join(".kube")).unwrap();
let fake_kubeconfig = fake_home.join(".kube").join("config");
fs::write(&fake_kubeconfig, "home dir kubeconfig content").unwrap();
unsafe { env::remove_var("KUBECONFIG") };
unsafe { env::set_var("HOME", fake_home.to_str().unwrap()) };
let home_fallback_result = get_kubeconfig_paths_from_option(None);
assert!(home_fallback_result.is_ok());
let home_fallback_path = home_fallback_result.unwrap();
assert!(!home_fallback_path.is_empty());
assert_eq!(home_fallback_path[0], fake_kubeconfig);
let nonexistent_dir = temp_dir.path().join("nonexistent");
unsafe { env::set_var("HOME", nonexistent_dir.to_str().unwrap()) };
unsafe {
env::set_var(
"KUBECONFIG",
temp_dir.path().join("nonexistent_file").to_str().unwrap(),
)
};
let error_result = get_kubeconfig_paths_from_option(None);
assert!(error_result.is_err());
match original_kubeconfig {
Some(val) => unsafe { env::set_var("KUBECONFIG", val) },
None => unsafe { env::remove_var("KUBECONFIG") },
}
match original_home {
Some(val) => unsafe { env::set_var("HOME", val) },
None => unsafe { env::remove_var("HOME") },
}
}
#[test]
fn test_merge_kubeconfigs_empty() {
let (config, errors) = merge_kubeconfigs(&[]).unwrap();
assert!(list_contexts(&config).is_empty());
assert!(errors.is_empty());
}
#[test]
fn test_merge_kubeconfigs_with_invalid_path() {
let paths = vec![PathBuf::from("/invalid/path/that/should/not/exist")];
let (config, errors) = merge_kubeconfigs(&paths).unwrap();
assert!(list_contexts(&config).is_empty());
assert!(!errors.is_empty());
}
#[test]
fn test_list_contexts() {
let kubeconfig = Kubeconfig {
contexts: vec![
kube::config::NamedContext {
name: "context1".to_string(),
context: Some(kube::config::Context::default()),
..Default::default()
},
kube::config::NamedContext {
name: "context2".to_string(),
context: Some(kube::config::Context::default()),
..Default::default()
},
],
..Default::default()
};
let contexts = crate::kube::operations::list_contexts(&kubeconfig);
assert_eq!(contexts.len(), 2);
assert_eq!(contexts[0], "context1");
assert_eq!(contexts[1], "context2");
}
#[test]
fn test_config_ext_clone() {
let mut config = Config::new("https://example.com".parse().unwrap());
config.accept_invalid_certs = false;
let cloned_config = config.clone_with_invalid_certs(true);
assert!(cloned_config.accept_invalid_certs);
let cloned_config_false = config.clone_with_invalid_certs(false);
assert!(!cloned_config_false.accept_invalid_certs);
}
#[tokio::test]
async fn test_create_config_with_context() {
let mut kubeconfig = Kubeconfig::default();
let context_name = "test-context";
let named_context = kube::config::NamedContext {
name: context_name.to_string(),
context: Some(kube::config::Context::default()),
..Default::default()
};
kubeconfig.contexts = vec![named_context];
let result = create_config_with_context(&kubeconfig, context_name).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_config_ignores_unused_non_rsa_pkcs8_key() {
use openssl::ec::{
EcGroup,
EcKey,
};
use openssl::nid::Nid;
use openssl::pkey::PKey;
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
let ec_key = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
let ec_key_data = encode_block(&ec_key.private_key_to_pem_pkcs8().unwrap());
let kubeconfig = Kubeconfig::from_yaml(&format!(
r#"
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server.com
contexts:
- name: token-context
context:
cluster: test-cluster
user: token-user
current-context: token-context
users:
- name: token-user
user:
token: test-token
- name: ec-user
user:
client-key-data: {ec_key_data}
"#
))
.unwrap();
let result = create_config_with_context(&kubeconfig, "token-context").await;
assert!(result.is_ok(), "{:#}", result.unwrap_err());
}
#[test]
fn test_config_ext_clone_preserves_other_fields() {
let mut config = Config::new("https://example.com".parse().unwrap());
config.accept_invalid_certs = false;
config.default_namespace = "test-namespace".to_string();
config.read_timeout = Some(std::time::Duration::from_secs(30));
let cloned_config = config.clone_with_invalid_certs(true);
assert!(cloned_config.accept_invalid_certs);
assert_eq!(
cloned_config.default_namespace,
"test-namespace".to_string()
);
assert_eq!(
cloned_config.read_timeout,
Some(std::time::Duration::from_secs(30))
);
assert_eq!(cloned_config.cluster_url, config.cluster_url);
}
#[tokio::test]
async fn test_create_config_with_insecure_skip_tls_verify_true() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let kubeconfig_path = temp_dir.path().join("kubeconfig");
let kubeconfig_content = r#"
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server.com
insecure-skip-tls-verify: true
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
users:
- name: test-user
user:
token: test-token
"#;
std::fs::write(&kubeconfig_path, kubeconfig_content).unwrap();
let kubeconfig = Kubeconfig::read_from(&kubeconfig_path).unwrap();
let config = create_config_with_context(&kubeconfig, "test-context")
.await
.unwrap();
assert!(config.accept_invalid_certs);
}
#[tokio::test]
async fn test_create_config_with_insecure_skip_tls_verify_false() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let kubeconfig_path = temp_dir.path().join("kubeconfig");
let kubeconfig_content = r#"
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server.com
insecure-skip-tls-verify: false
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
users:
- name: test-user
user:
token: test-token
"#;
std::fs::write(&kubeconfig_path, kubeconfig_content).unwrap();
let kubeconfig = Kubeconfig::read_from(&kubeconfig_path).unwrap();
let config = create_config_with_context(&kubeconfig, "test-context")
.await
.unwrap();
assert!(!config.accept_invalid_certs);
}
#[tokio::test]
async fn test_create_config_without_insecure_skip_tls_verify() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let kubeconfig_path = temp_dir.path().join("kubeconfig");
let kubeconfig_content = r#"
apiVersion: v1
kind: Config
clusters:
- name: test-cluster
cluster:
server: https://test-server.com
contexts:
- name: test-context
context:
cluster: test-cluster
user: test-user
current-context: test-context
users:
- name: test-user
user:
token: test-token
"#;
std::fs::write(&kubeconfig_path, kubeconfig_content).unwrap();
let kubeconfig = Kubeconfig::read_from(&kubeconfig_path).unwrap();
let config = create_config_with_context(&kubeconfig, "test-context")
.await
.unwrap();
assert!(!config.accept_invalid_certs);
}
#[tokio::test]
async fn test_merge_kubeconfigs_with_insecure_skip_tls() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let kubeconfig_path = temp_dir.path().join("kubeconfig");
let kubeconfig_content = r#"
apiVersion: v1
kind: Config
clusters:
- name: secure-cluster
cluster:
server: https://secure-server.com
insecure-skip-tls-verify: false
- name: insecure-cluster
cluster:
server: https://insecure-server.com
insecure-skip-tls-verify: true
contexts:
- name: secure-context
context:
cluster: secure-cluster
user: test-user
- name: insecure-context
context:
cluster: insecure-cluster
user: test-user
users:
- name: test-user
user:
token: test-token
"#;
std::fs::write(&kubeconfig_path, kubeconfig_content).unwrap();
let (merged_kubeconfig, errors) = merge_kubeconfigs(&[kubeconfig_path]).unwrap();
let contexts = list_contexts(&merged_kubeconfig);
assert_eq!(contexts.len(), 2);
assert!(contexts.contains(&"secure-context".to_string()));
assert!(contexts.contains(&"insecure-context".to_string()));
assert!(errors.is_empty());
let insecure_cluster = merged_kubeconfig
.clusters
.iter()
.find(|c| c.name == "insecure-cluster")
.unwrap();
assert_eq!(
insecure_cluster
.cluster
.as_ref()
.unwrap()
.insecure_skip_tls_verify,
Some(true)
);
let secure_cluster = merged_kubeconfig
.clusters
.iter()
.find(|c| c.name == "secure-cluster")
.unwrap();
assert_eq!(
secure_cluster
.cluster
.as_ref()
.unwrap()
.insecure_skip_tls_verify,
Some(false)
);
}
}