Skip to content

Commit

Permalink
fix: handle cases where the kubeconfig contains multiple paths (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
hcavarsan authored Aug 20, 2024
1 parent 6c057a7 commit 542b20d
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 184 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/kftray-commons/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ tauri = { version = "1.6", default-features = false, features = [
] }
sqlx = { version = "0.8.0", default-features = false, features = ["sqlite", "runtime-tokio-native-tls"] }
hostsfile = { git = "https://github.com/tonarino/innernet", branch = "main" }
tempfile = "3.12.0"

[lib]
name = "kftray_commons"
Expand Down
68 changes: 43 additions & 25 deletions crates/kftray-commons/src/utils/config_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,40 @@ pub fn get_window_state_path() -> Result<PathBuf, String> {
Ok(config_path)
}

pub fn get_default_kubeconfig_path() -> Result<PathBuf> {
if let Ok(kubeconfig_path) = env::var("KUBECONFIG") {
Ok(PathBuf::from(kubeconfig_path))
} else if let Some(mut config_path) = dirs::home_dir() {
config_path.push(".kube/config");
Ok(config_path)
pub fn get_kubeconfig_paths() -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();

if let Ok(kubeconfig_paths) = env::var("KUBECONFIG") {
for path in kubeconfig_paths.split(if cfg!(windows) { ';' } else { ':' }) {
let path_buf = PathBuf::from(path);
if path_buf.exists() {
paths.push(path_buf);
}
}
}

if paths.is_empty() {
if let Some(mut config_path) = dirs::home_dir() {
config_path.push(".kube/config");
if config_path.exists() {
paths.push(config_path);
}
}
}

if paths.is_empty() {
Err(anyhow::anyhow!("Unable to determine kubeconfig path"))
} else {
Err(anyhow::anyhow!("Unable to determine home directory"))
Ok(paths)
}
}

#[cfg(test)]
mod tests {
use std::env;
use std::fs;
use std::path::PathBuf;

use tempfile::TempDir;

use super::*;

Expand Down Expand Up @@ -108,7 +128,7 @@ mod tests {
env::remove_var("KFTRAY_CONFIG");
env::set_var("XDG_CONFIG_HOME", "/xdg/config/home");
let config_dir = get_config_dir().unwrap();
assert_eq!(config_dir, PathBuf::from("/xdg/config/home/.kftray"));
assert_eq!(config_dir, PathBuf::from("/xdg/config/home/kftray"));

restore_env_vars(preserved_vars);
}
Expand All @@ -126,16 +146,6 @@ mod tests {
restore_env_vars(preserved_vars);
}

#[test]
fn test_get_config_dir_missing_home() {
let preserved_vars = preserve_env_vars(&["KFTRAY_CONFIG", "XDG_CONFIG_HOME"]);

env::remove_var("KFTRAY_CONFIG");
env::remove_var("XDG_CONFIG_HOME");

restore_env_vars(preserved_vars);
}

#[test]
fn test_get_log_folder_path() {
let preserved_vars = preserve_env_vars(&["KFTRAY_CONFIG"]);
Expand Down Expand Up @@ -201,18 +211,26 @@ mod tests {
}

#[test]
fn test_get_default_kubeconfig_path() {
fn test_get_kubeconfig_paths() {
let preserved_vars = preserve_env_vars(&["KUBECONFIG"]);

env::set_var("KUBECONFIG", "/custom/kube/config");
let kubeconfig_path = get_default_kubeconfig_path().unwrap();
assert_eq!(kubeconfig_path, PathBuf::from("/custom/kube/config"));
let temp_dir = TempDir::new().unwrap();
let custom_kubeconfig_path = temp_dir.path().join("custom_kube_config");
fs::write(&custom_kubeconfig_path, "mock kubeconfig content").unwrap();

env::set_var("KUBECONFIG", custom_kubeconfig_path.to_str().unwrap());
let kubeconfig_paths = get_kubeconfig_paths().unwrap();
assert_eq!(kubeconfig_paths, vec![custom_kubeconfig_path.clone()]);

env::remove_var("KUBECONFIG");
let expected_default_path = dirs::home_dir().unwrap().join(".kube/config");
let kubeconfig_path = get_default_kubeconfig_path().unwrap();
assert_eq!(kubeconfig_path, expected_default_path);
fs::write(&expected_default_path, "mock kubeconfig content").unwrap();
let kubeconfig_paths = get_kubeconfig_paths().unwrap();
assert_eq!(kubeconfig_paths, vec![expected_default_path.clone()]);

restore_env_vars(preserved_vars);

fs::remove_file(custom_kubeconfig_path).unwrap();
fs::remove_file(expected_default_path).unwrap();
}
}
1 change: 1 addition & 0 deletions crates/kftray-portforward/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ hostsfile = { git = "https://github.com/tonarino/innernet", branch = "main" }
kftray-commons = { path = "../kftray-commons" }
tower = "0.4.13"
hyper-util = "0.1.7"
dirs = "5.0.1"

[lib]
name = "kftray_portforward"
Expand Down
145 changes: 97 additions & 48 deletions crates/kftray-portforward/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::path::PathBuf;

use anyhow::{
Context,
Result,
};
use hyper_util::rt::TokioExecutor;
use kftray_commons::config_dir::get_default_kubeconfig_path;
use kftray_commons::config_dir::get_kubeconfig_paths;
use kube::{
client::ConfigExt,
config::{
Expand All @@ -17,59 +19,106 @@ use log::info;
use tower::ServiceBuilder;

pub async fn create_client_with_specific_context(
kubeconfig: Option<String>, context_name: &str,
) -> Result<Client> {
let kubeconfig = if let Some(path) = kubeconfig {
if path == "default" {
let default_path = get_default_kubeconfig_path()?;

info!(
"Reading kubeconfig from default location: {:?}",
default_path
);

Kubeconfig::read_from(default_path)
.context("Failed to read kubeconfig from default location")?
} else {
info!("Reading kubeconfig from specified path: {}", path);
kubeconfig: Option<String>, context_name: Option<&str>,
) -> Result<(Option<Client>, Option<Kubeconfig>, Vec<String>)> {
let kubeconfig_paths = match kubeconfig {
Some(path) if path == "default" => get_kubeconfig_paths()?,
Some(path) => path.split(':').map(PathBuf::from).collect(),
None => get_kubeconfig_paths()?,
};

Kubeconfig::read_from(path).context("Failed to read kubeconfig from specified path")?
}
} else {
let default_path = get_default_kubeconfig_path()?;
let mut errors = Vec::new();
let mut all_contexts = Vec::new();

info!(
"Reading kubeconfig from default location: {:?}",
default_path
);
for path in &kubeconfig_paths {
info!("Trying kubeconfig path: {:?}", path);

Kubeconfig::read_from(default_path)
.context("Failed to read kubeconfig from default location")?
};
match Kubeconfig::read_from(path)
.context(format!("Failed to read kubeconfig from {:?}", path))
{
Ok(kubeconfig) => {
info!("Successfully read kubeconfig from {:?}", path);
let contexts = list_contexts(&kubeconfig);
all_contexts.extend(contexts.clone());
info!("Available contexts: {:?}", contexts);

let config = Config::from_custom_kubeconfig(
kubeconfig,
&KubeConfigOptions {
context: Some(context_name.to_owned()),
..Default::default()
},
)
.await
.context("Failed to create configuration from kubeconfig")?;
if let Some(context_name) = context_name {
match Config::from_custom_kubeconfig(
kubeconfig.clone(),
&KubeConfigOptions {
context: Some(context_name.to_owned()),
..Default::default()
},
)
.await
.context("Failed to create configuration from kubeconfig")
{
Ok(config) => {
info!(
"Successfully created configuration for context: {}",
context_name
);
match config
.rustls_https_connector()
.context("Failed to create Rustls HTTPS connector")
{
Ok(https_connector) => {
let service = ServiceBuilder::new()
.layer(config.base_uri_layer())
.option_layer(config.auth_layer()?)
.service(
hyper_util::client::legacy::Client::builder(
TokioExecutor::new(),
)
.build(https_connector),
);

let https_connector = config
.rustls_https_connector()
.context("Failed to create Rustls HTTPS connector")?;
let client = Client::new(service, config.default_namespace);
return Ok((Some(client), Some(kubeconfig), all_contexts));
}
Err(e) => {
let error_msg = format!(
"Failed to create Rustls HTTPS connector for {:?}: {}",
path, e
);
info!("{}", error_msg);
errors.push(error_msg);
}
}
}
Err(e) => {
let error_msg = format!(
"Failed to create configuration from kubeconfig for {:?}: {}",
path, e
);
info!("{}", error_msg);
errors.push(error_msg);
}
}
}
}
Err(e) => {
let error_msg = format!("Failed to read kubeconfig from {:?}: {}", path, e);
info!("{}", error_msg);
errors.push(error_msg);
}
}
}

let service = ServiceBuilder::new()
.layer(config.base_uri_layer())
.option_layer(config.auth_layer()?)
.service(
hyper_util::client::legacy::Client::builder(TokioExecutor::new())
.build(https_connector),
);
if context_name.is_none() {
return Ok((None, None, all_contexts));
}

let client = Client::new(service, config.default_namespace);
Err(anyhow::anyhow!(
"Unable to create client with any of the provided kubeconfig paths: {}",
errors.join("; ")
))
}

Ok(client)
fn list_contexts(kubeconfig: &Kubeconfig) -> Vec<String> {
kubeconfig
.contexts
.iter()
.map(|context| context.name.clone())
.collect()
}
13 changes: 10 additions & 3 deletions crates/kftray-portforward/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,16 +555,23 @@ pub async fn deploy_and_forward_pod(
let mut responses: Vec<CustomResponse> = Vec::new();

for mut config in configs.into_iter() {
let client = if !config.context.is_empty() {
let (client, _, _) = if !config.context.is_empty() {
let kubeconfig = config.kubeconfig.clone();

create_client_with_specific_context(kubeconfig, &config.context)
create_client_with_specific_context(kubeconfig, Some(&config.context))
.await
.map_err(|e| e.to_string())?
} else {
Client::try_default().await.map_err(|e| e.to_string())?
(
Some(Client::try_default().await.map_err(|e| e.to_string())?),
None,
Vec::new(),
)
};

let client =
client.ok_or_else(|| format!("Client not created for context '{}'", config.context))?;

let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| e.to_string())?
Expand Down
1 change: 0 additions & 1 deletion crates/kftray-portforward/src/models/kube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ impl Target {
let spec = pod.spec.as_ref().context("Pod Spec is None")?;
let containers = &spec.containers;

// Find the port by name within the container ports
containers
.iter()
.flat_map(|c| c.ports.as_ref().map_or(Vec::new(), |v| v.clone()))
Expand Down
1 change: 0 additions & 1 deletion crates/kftray-portforward/src/pod_finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ impl<'a> TargetPodFinder<'a> {
async fn find_pod_by_label(
&self, label: &str, ready_pod: &AnyReady, target: &Target,
) -> Result<TargetPod> {
// Directly use the label provided
let label_selector_str = label.to_string();
let pods = self
.pod_api
Expand Down
15 changes: 11 additions & 4 deletions crates/kftray-portforward/src/port_forward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,19 @@ impl PortForward {
local_address: impl Into<Option<String>>, context_name: Option<String>,
kubeconfig: Option<String>, config_id: i64, workload_type: String,
) -> anyhow::Result<Self> {
let client = if let Some(ref context_name) = context_name {
crate::client::create_client_with_specific_context(kubeconfig, context_name).await?
let (client, _, _) = if let Some(ref context_name) = context_name {
crate::client::create_client_with_specific_context(kubeconfig, Some(context_name))
.await?
} else {
Client::try_default().await?
(Some(Client::try_default().await?), None, Vec::new())
};

let client = client.ok_or_else(|| {
anyhow::anyhow!(
"Client not created for context '{}'",
context_name.clone().unwrap_or_default()
)
})?;
let namespace = target.namespace.name_any();

Ok(Self {
Expand All @@ -71,7 +78,7 @@ impl PortForward {
local_address: local_address.into(),
pod_api: Api::namespaced(client.clone(), &namespace),
svc_api: Api::namespaced(client, &namespace),
context_name,
context_name: context_name.clone(),
config_id,
workload_type,
connection: Arc::new(Mutex::new(None)),
Expand Down
1 change: 0 additions & 1 deletion crates/kftray-server/src/udp_over_tcp_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ fn handle_udp_to_tcp(
}
}
Err(e) => {
// Handling the would block error that is normal for non-blocking IO
if e.kind() != io::ErrorKind::WouldBlock {
error!("UDP to TCP: Error receiving from UDP socket: {}", e);
}
Expand Down
Loading

0 comments on commit 542b20d

Please sign in to comment.