use colored::*;
use kube::{Api, Client};
use k8s_openapi::api::core::v1::{Pod, Node};
use std::time::Duration;
use tokio::time::timeout;
use crate::errors::{NetInspectError, NetInspectResult};
use crate::validation::Validator;
pub async fn diagnose(namespace: Option<&str>) -> NetInspectResult<()> {
println!("{}", "🔍 Starting network diagnosis...".cyan().bold());
let client = create_kubernetes_client().await?;
let cni_result = timeout(
Duration::from_secs(30),
detect_cni(&client)
).await;
let cni_type = match cni_result {
Ok(Ok(cni)) => cni,
Ok(Err(e)) => return Err(e),
Err(_) => return Err(NetInspectError::Timeout(
"CNI detection timed out after 30 seconds".to_string()
)),
};
println!("{} CNI detected: {}", "✓".green().bold(), cni_type.green());
let nodes_result = timeout(
Duration::from_secs(15),
get_cluster_nodes(&client)
).await;
let node_count = match nodes_result {
Ok(Ok(count)) => count,
Ok(Err(e)) => return Err(e),
Err(_) => return Err(NetInspectError::Timeout(
"Node listing timed out after 15 seconds".to_string()
)),
};
if node_count == 0 {
println!("{} {}", "⚠".yellow().bold(), "No nodes found in cluster".yellow());
} else {
println!("{} Found {} nodes", "✓".green().bold(), node_count.to_string().yellow());
}
let pod_result = timeout(
Duration::from_secs(15),
check_pods_in_namespace(&client, namespace)
).await;
match pod_result {
Ok(Ok(pod_count)) => {
if let Some(ns) = namespace {
println!("{} Found {} pods in namespace '{}'",
"✓".green().bold(),
pod_count.to_string().yellow(),
ns.yellow());
} else {
println!("{} Found {} pods cluster-wide",
"✓".green().bold(),
pod_count.to_string().yellow());
}
},
Ok(Err(e)) => {
println!("{} Failed to check pods: {}", "⚠".yellow().bold(), e);
},
Err(_) => {
println!("{} Pod listing timed out after 15 seconds", "⚠".yellow().bold());
}
}
Ok(())
}
pub async fn test_pod(pod_name: &str, namespace: &str) -> NetInspectResult<()> {
println!("{} Testing connectivity for pod: {}/{}",
"🔍".cyan(), namespace.yellow(), pod_name.yellow());
let client = create_kubernetes_client().await?;
let pods: Api<Pod> = Api::namespaced(client, namespace);
let pod_result = timeout(
Duration::from_secs(10),
pods.get(pod_name)
).await;
let pod = match pod_result {
Ok(Ok(pod)) => pod,
Ok(Err(kube::Error::Api(api_err))) if api_err.code == 404 => {
return Err(NetInspectError::ResourceNotFound(
format!("Pod '{}' not found in namespace '{}'", pod_name, namespace)
));
},
Ok(Err(e)) => return Err(NetInspectError::from(e)),
Err(_) => return Err(NetInspectError::Timeout(
"Pod lookup timed out after 10 seconds".to_string()
)),
};
let status = pod.status.as_ref().ok_or_else(|| {
NetInspectError::ResourceNotFound(
format!("Pod '{}' has no status information - it may be initializing", pod_name)
)
})?;
if let Some(phase) = &status.phase {
match phase.as_str() {
"Pending" => {
println!("{} Pod is in Pending phase - not yet scheduled", "⚠".yellow().bold());
return Err(NetInspectError::ResourceNotFound(
"Pod is pending and has no IP address yet".to_string()
));
},
"Failed" | "Succeeded" => {
println!("{} Pod is in {} phase - not running", "⚠".yellow().bold(), phase);
return Err(NetInspectError::ResourceNotFound(
format!("Pod is in {} phase and cannot be tested", phase)
));
},
"Running" => {
println!("{} Pod is running", "✓".green().bold());
},
_ => {
println!("{} Pod phase: {}", "ℹ".blue().bold(), phase.yellow());
}
}
}
let pod_ip = status.pod_ip.as_ref().ok_or_else(|| {
NetInspectError::ResourceNotFound(
format!("Pod '{}' has no IP address assigned - check if it's running", pod_name)
)
})?;
Validator::validate_pod_ip(pod_ip)?;
println!("{} Pod IP: {}", "ℹ".blue().bold(), pod_ip.cyan());
match test_connectivity_with_retries(pod_ip, 3).await {
Ok(()) => {
println!("{} Connectivity test: {}", "✓".green().bold(), "PASS".green().bold());
Ok(())
}
Err(e) => {
println!("{} Connectivity test: {} - {}", "✗".red().bold(), "FAIL".red().bold(), e);
Err(e)
}
}
}
pub fn version() {
println!("{} k8s-netinspect v{}",
"🔧".yellow().bold(),
env!("CARGO_PKG_VERSION").green());
println!("A minimal Kubernetes network inspection tool");
}
async fn detect_cni(client: &Client) -> NetInspectResult<String> {
let nodes_list = get_cluster_nodes_list(client).await?;
if nodes_list.is_empty() {
return Ok("No nodes available for CNI detection".to_string());
}
let mut detected_cnis = Vec::new();
for node in &nodes_list {
if let Some(status) = &node.status {
if let Some(node_info) = &status.node_info {
let runtime = &node_info.container_runtime_version;
if let Some(annotations) = &node.metadata.annotations {
if annotations.keys().any(|k| k.contains("calico") || k.contains("projectcalico")) {
detected_cnis.push("Calico".to_string());
continue;
}
if annotations.keys().any(|k| k.contains("flannel")) {
detected_cnis.push("Flannel".to_string());
continue;
}
if annotations.keys().any(|k| k.contains("weave")) {
detected_cnis.push("Weave Net".to_string());
continue;
}
if annotations.keys().any(|k| k.contains("cilium")) {
detected_cnis.push("Cilium".to_string());
continue;
}
}
if runtime.contains("containerd") {
detected_cnis.push("Generic CNI (containerd)".to_string());
} else if runtime.contains("docker") {
detected_cnis.push("Generic CNI (docker)".to_string());
}
}
}
}
if detected_cnis.is_empty() {
Ok("Unknown CNI".to_string())
} else {
Ok(detected_cnis.into_iter().next().unwrap())
}
}
async fn test_connectivity_with_retries(pod_ip: &str, max_retries: u32) -> NetInspectResult<()> {
for attempt in 1..=max_retries {
match test_connectivity(pod_ip).await {
Ok(()) => return Ok(()),
Err(e) => {
if attempt < max_retries {
println!("{} Attempt {} failed, retrying... ({})",
"⚠".yellow().bold(), attempt, e);
tokio::time::sleep(Duration::from_millis(1000 * attempt as u64)).await;
} else {
return Err(e);
}
}
}
}
unreachable!()
}
async fn test_connectivity(pod_ip: &str) -> NetInspectResult<()> {
let url = format!("http://{}:80", pod_ip);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.connect_timeout(Duration::from_secs(5))
.build()
.map_err(|e| NetInspectError::Runtime(
format!("Failed to create HTTP client: {}", e)
))?;
let response = client.get(&url).send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(NetInspectError::NetworkConnectivity(
format!("HTTP {} - {}",
response.status(),
response.status().canonical_reason().unwrap_or("Unknown error"))
))
}
}
async fn create_kubernetes_client() -> NetInspectResult<Client> {
Client::try_default().await.map_err(NetInspectError::from)
}
async fn get_cluster_nodes(client: &Client) -> NetInspectResult<usize> {
let nodes: Api<Node> = Api::all(client.clone());
let node_list = nodes.list(&Default::default()).await
.map_err(NetInspectError::from)?;
Ok(node_list.items.len())
}
async fn get_cluster_nodes_list(client: &Client) -> NetInspectResult<Vec<Node>> {
let nodes: Api<Node> = Api::all(client.clone());
let node_list = nodes.list(&Default::default()).await
.map_err(NetInspectError::from)?;
Ok(node_list.items)
}
async fn check_pods_in_namespace(client: &Client, namespace: Option<&str>) -> NetInspectResult<usize> {
let pods = if let Some(ns) = namespace {
let pods: Api<Pod> = Api::namespaced(client.clone(), ns);
pods.list(&Default::default()).await
.map_err(NetInspectError::from)?
} else {
let pods: Api<Pod> = Api::all(client.clone());
pods.list(&Default::default()).await
.map_err(NetInspectError::from)?
};
Ok(pods.items.len())
}
async fn test_connectivity_quick(pod_ip: &str) -> NetInspectResult<()> {
let url = format!("http://{}:80", pod_ip);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(3)) .connect_timeout(Duration::from_secs(2))
.build()
.map_err(|e| NetInspectError::Runtime(
format!("Failed to create HTTP client: {}", e)
))?;
let response = client.get(&url).send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(NetInspectError::NetworkConnectivity(
format!("HTTP {} - {}",
response.status(),
response.status().canonical_reason().unwrap_or("Unknown error"))
))
}
}