Skip to content

Commit

Permalink
add auto import based on kube annotations (#300)
Browse files Browse the repository at this point in the history
* feat: auto import configs based on annotations

* feat: auto import configs based on annotations
  • Loading branch information
hcavarsan authored Sep 7, 2024
1 parent 36f2948 commit 379e834
Show file tree
Hide file tree
Showing 13 changed files with 590 additions and 91 deletions.
111 changes: 111 additions & 0 deletions crates/kftray-portforward/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::error::Error;
use std::path::PathBuf;

Expand All @@ -6,7 +7,13 @@ use anyhow::{
Result,
};
use hyper_util::rt::TokioExecutor;
use k8s_openapi::api::core::v1::Namespace;
use k8s_openapi::api::core::v1::Service;
use k8s_openapi::api::core::v1::ServiceSpec;
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
use kftray_commons::config_dir::get_kubeconfig_paths;
use kube::api::ListParams;
use kube::Api;
use kube::{
client::ConfigExt,
config::{
Expand All @@ -23,6 +30,8 @@ use log::{
};
use tower::ServiceBuilder;

use crate::models::kube::KubeContextInfo;

pub async fn create_client_with_specific_context(
kubeconfig: Option<String>, context_name: Option<&str>,
) -> Result<(Option<Client>, Option<Kubeconfig>, Vec<String>)> {
Expand Down Expand Up @@ -174,3 +183,105 @@ fn list_contexts(kubeconfig: &Kubeconfig) -> Vec<String> {
.map(|context| context.name.clone())
.collect()
}

pub async fn list_kube_contexts(
kubeconfig: Option<String>,
) -> Result<Vec<KubeContextInfo>, String> {
info!("list_kube_contexts {}", kubeconfig.as_deref().unwrap_or(""));

let (_, kubeconfig, contexts) = create_client_with_specific_context(kubeconfig, None)
.await
.map_err(|err| format!("Failed to create client: {}", err))?;

if let Some(kubeconfig) = kubeconfig {
let contexts: Vec<KubeContextInfo> = kubeconfig
.contexts
.into_iter()
.map(|c| KubeContextInfo { name: c.name })
.collect();

Ok(contexts)
} else if !contexts.is_empty() {
let context_infos: Vec<KubeContextInfo> = contexts
.into_iter()
.map(|name| KubeContextInfo { name })
.collect();

Ok(context_infos)
} else {
Err("Failed to retrieve kubeconfig".to_string())
}
}

pub async fn list_all_namespaces(client: Client) -> Result<Vec<String>, anyhow::Error> {
let namespaces: Api<Namespace> = Api::all(client);
let namespace_list = namespaces.list(&ListParams::default()).await?;

let mut namespace_names = Vec::new();
for namespace in namespace_list {
if let Some(name) = namespace.metadata.name {
namespace_names.push(name);
}
}

Ok(namespace_names)
}
pub async fn get_services_with_annotation(
client: Client, namespace: &str, _: &str,
) -> Result<Vec<(String, HashMap<String, String>, HashMap<String, i32>)>, Box<dyn std::error::Error>>
{
let services: Api<Service> = Api::namespaced(client, namespace);
let lp = ListParams::default();

let service_list = services.list(&lp).await?;

let mut results = Vec::new();

for service in service_list {
if let Some(service_name) = service.metadata.name.clone() {
if let Some(annotations) = &service.metadata.annotations {
if annotations
.get("kftray.app/enabled")
.map_or(false, |v| v == "true")
{
let ports = extract_ports_from_service(&service);
let annotations_hashmap: HashMap<String, String> =
annotations.clone().into_iter().collect();
results.push((service_name, annotations_hashmap, ports));
}
}
}
}

Ok(results)
}

fn extract_ports_from_service(service: &Service) -> HashMap<String, i32> {
let mut ports = HashMap::new();
if let Some(spec) = &service.spec {
for port in spec.ports.as_ref().unwrap_or(&vec![]) {
let port_number = match port.target_port {
Some(IntOrString::Int(port)) => port,
Some(IntOrString::String(ref name)) => {
resolve_named_port(spec, name).unwrap_or_default()
}
None => continue,
};
ports.insert(
port.name.clone().unwrap_or_else(|| port_number.to_string()),
port_number,
);
}
}
ports
}

fn resolve_named_port(spec: &ServiceSpec, name: &str) -> Option<i32> {
spec.ports.as_ref()?.iter().find_map(|port| {
if port.name.as_deref() == Some(name) {
Some(port.port)
} else {
None
}
})
}
101 changes: 101 additions & 0 deletions crates/kftray-portforward/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ use rand::{
use tokio::task::JoinHandle;

use crate::client::create_client_with_specific_context;
use crate::client::{
get_services_with_annotation,
list_all_namespaces,
};
use crate::models::kube::{
HttpLogState,
Port,
Expand Down Expand Up @@ -785,3 +789,100 @@ pub async fn stop_proxy_forward(

Ok(stop_result)
}

pub async fn retrieve_service_configs(context: &str) -> Result<Vec<Config>, String> {
let (client, _, _) = create_client_with_specific_context(None, Some(context))
.await
.map_err(|e| e.to_string())?;

let client = client.ok_or_else(|| "Client not created".to_string())?;
let annotation = "kftray.app/configs";

let namespaces = list_all_namespaces(client.clone())
.await
.map_err(|e| e.to_string())?;
let mut configs = Vec::new();

for namespace in namespaces {
let services = get_services_with_annotation(client.clone(), &namespace, annotation)
.await
.map_err(|e| e.to_string())?;
for (service_name, annotations, ports) in services {
if let Some(configs_str) = annotations.get(annotation) {
configs.extend(parse_configs(
configs_str,
context,
&namespace,
&service_name,
&ports,
));
} else {
configs.extend(create_default_configs(
context,
&namespace,
&service_name,
&ports,
));
}
}
}

Ok(configs)
}

fn parse_configs(
configs_str: &str, context: &str, namespace: &str, service_name: &str,
ports: &HashMap<String, i32>,
) -> Vec<Config> {
configs_str
.split(',')
.filter_map(|config_str| {
let parts: Vec<&str> = config_str.trim().split('-').collect();
if parts.len() != 3 {
return None;
}

let alias = parts[0].to_string();
let local_port: u16 = parts[1].parse().ok()?;
let target_port = parts[2]
.parse()
.ok()
.or_else(|| ports.get(parts[2]).cloned())?;

Some(Config {
id: None,
context: context.to_string(),
kubeconfig: None,
namespace: namespace.to_string(),
service: Some(service_name.to_string()),
alias: Some(alias),
local_port,
remote_port: target_port as u16,
protocol: "tcp".to_string(),
workload_type: "service".to_string(),
..Default::default()
})
})
.collect()
}

fn create_default_configs(
context: &str, namespace: &str, service_name: &str, ports: &HashMap<String, i32>,
) -> Vec<Config> {
ports
.iter()
.map(|(_port_name, &port)| Config {
id: None,
context: context.to_string(),
kubeconfig: None,
namespace: namespace.to_string(),
service: Some(service_name.to_string()),
alias: Some(service_name.to_string()),
local_port: port as u16,
remote_port: port as u16,
protocol: "tcp".to_string(),
workload_type: "service".to_string(),
..Default::default()
})
.collect()
}
9 changes: 9 additions & 0 deletions crates/kftray-tauri/src/commands/kubecontext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use k8s_openapi::api::core::v1::{
Service,
};
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
use kftray_commons::config_model::Config;
use kftray_portforward::client::create_client_with_specific_context;
use kftray_portforward::core::retrieve_service_configs;
use kftray_portforward::models::kube::{
KubeContextInfo,
KubeNamespaceInfo,
Expand Down Expand Up @@ -271,3 +273,10 @@ pub async fn list_ports(
}
}
}

#[tauri::command]
pub async fn get_services_with_annotations(context_name: &str) -> Result<Vec<Config>, String> {
info!("get_services_with_annotations for context {}", context_name);

retrieve_service_configs(context_name).await
}
1 change: 1 addition & 0 deletions crates/kftray-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ fn main() {
commands::kubecontext::list_services,
commands::kubecontext::list_pods,
commands::kubecontext::list_ports,
commands::kubecontext::get_services_with_annotations,
commands::portforward::deploy_and_forward_pod_cmd,
commands::portforward::stop_proxy_forward_cmd,
commands::httplogs::set_http_logs_cmd,
Expand Down
77 changes: 0 additions & 77 deletions crates/kftui/src/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,3 @@
# will have compiled files and executables
/target/


use std::sync::Arc;

use kftray_commons::models::config_model::Config;
use kftray_portforward::core::{
deploy_and_forward_pod,
start_port_forward,
stop_port_forward,
stop_proxy_forward,
};
use kftray_portforward::models::kube::HttpLogState;
use log::error;

use crate::tui::input::App;

pub async fn start_port_forwarding(app: &mut App, config: Config) {
match config.workload_type.as_str() {
"proxy" => {
if let Err(e) =
deploy_and_forward_pod(vec![config.clone()], Arc::new(HttpLogState::new())).await
{
error!("Failed to start proxy forward: {:?}", e);
app.error_message = Some(format!("Failed to start proxy forward: {:?}", e));
app.show_error_popup = true;
}
}
"service" | "pod" => match config.protocol.as_str() {
"tcp" => {
let log_state = Arc::new(HttpLogState::new());
let result = start_port_forward(vec![config.clone()], "tcp", log_state).await;
if let Err(e) = result {
error!("Failed to start TCP port forward: {:?}", e);
app.error_message = Some(format!("Failed to start TCP port forward: {:?}", e));
app.show_error_popup = true;
}
}
"udp" => {
let result =
deploy_and_forward_pod(vec![config.clone()], Arc::new(HttpLogState::new()))
.await;
if let Err(e) = result {
error!("Failed to start UDP port forward: {:?}", e);
app.error_message = Some(format!("Failed to start UDP port forward: {:?}", e));
app.show_error_popup = true;
}
}
_ => {}
},
_ => {}
}
}

pub async fn stop_port_forwarding(app: &mut App, config: Config) {
match config.workload_type.as_str() {
"proxy" => {
if let Err(e) = stop_proxy_forward(
config.id.unwrap_or_default().to_string(),
&config.namespace,
config.service.clone().unwrap_or_default(),
)
.await
{
error!("Failed to stop proxy forward: {:?}", e);
app.error_message = Some(format!("Failed to stop proxy forward: {:?}", e));
app.show_error_popup = true;
}
}
"service" | "pod" => {
if let Err(e) = stop_port_forward(config.id.unwrap_or_default().to_string()).await {
error!("Failed to stop port forward: {:?}", e);
app.error_message = Some(format!("Failed to stop port forward: {:?}", e));
app.show_error_popup = true;
}
}
_ => {}
}
}
Loading

0 comments on commit 379e834

Please sign in to comment.