diff --git a/cli/src/logs.rs b/cli/src/logs.rs new file mode 100644 index 0000000..ce496a0 --- /dev/null +++ b/cli/src/logs.rs @@ -0,0 +1,196 @@ +use std::process::Command; +use std::str; + +#[derive(Debug, Clone)] +pub enum Component { + ControlPlane, + DataPlane, +} + +impl From for Component { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "control-plane" => Component::ControlPlane, + "data-plane" => Component::DataPlane, + //default will be control plane. + _ => Component::ControlPlane, + } + } +} + +impl Component { + fn to_label_selector(&self) -> &str { + match self { + Component::ControlPlane => "component=control-plane", + Component::DataPlane => "component=data-plane", + } + } +} + +fn check_namespace_exists(namespace: &str) -> bool { + let output = Command::new("kubectl") + .args(["get", "namespace", namespace]) + .output(); + + match output { + Ok(output) => output.status.success(), + Err(_) => false, + } +} + +fn get_available_namespaces() -> Vec { + let output = Command::new("kubectl") + .args(["get", "namespaces", "--no-headers", "-o", "custom-columns=NAME:.metadata.name"]) + .output(); + + match output { + Ok(output) if output.status.success() => { + let stdout = str::from_utf8(&output.stdout).unwrap_or(""); + stdout.lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect() + } + _ => Vec::new(), + } +} + +fn get_pods_for_service(namespace: &str, service_name: &str) -> Vec { + let output = Command::new("kubectl") + .args(["get", "pods", "-n", namespace, "-l", &format!("app={}", service_name), "--no-headers", "-o", "custom-columns=NAME:.metadata.name"]) + .output(); + + match output { + Ok(output) if output.status.success() => { + let stdout = str::from_utf8(&output.stdout).unwrap_or(""); + stdout.lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect() + } + _ => Vec::new(), + } +} + +fn get_pods_for_component(namespace: &str, component: &Component) -> Vec { + let output = Command::new("kubectl") + .args(["get", "pods", "-n", namespace, "-l", component.to_label_selector(), "--no-headers", "-o", "custom-columns=NAME:.metadata.name"]) + .output(); + + match output { + Ok(output) if output.status.success() => { + let stdout = str::from_utf8(&output.stdout).unwrap_or(""); + stdout.lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect() + } + _ => Vec::new(), + } +} + +fn get_all_pods(namespace: &str) -> Vec { + let output = Command::new("kubectl") + .args(["get", "pods", "-n", namespace, "--no-headers", "-o", "custom-columns=NAME:.metadata.name"]) + .output(); + + match output { + Ok(output) if output.status.success() => { + let stdout = str::from_utf8(&output.stdout).unwrap_or(""); + stdout.lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect() + } + _ => Vec::new(), + } +} + +pub fn logs_command(service: Option, component: Option, namespace: Option) { + let ns = namespace.unwrap_or_else(|| "cortexflow".to_string()); + + // namespace check + if !check_namespace_exists(&ns) { + let available_namespaces = get_available_namespaces(); + + println!("\nāŒ Namespace '{}' not found", ns); + println!("{}", "=".repeat(50)); + + if !available_namespaces.is_empty() { + println!("\nšŸ“‹ Available namespaces:"); + for available_ns in &available_namespaces { + println!(" • {}", available_ns); + } + } else { + println!("No namespaces found in the cluster."); + } + + std::process::exit(1); + } + + // determine pods. + let pods = match (service, component) { + (Some(service_name), Some(component_str)) => { + let comp = Component::from(component_str); + println!("Getting logs for service '{}' with component '{:?}' in namespace '{}'", service_name, comp, ns); + + let service_pods = get_pods_for_service(&ns, &service_name); + let component_pods = get_pods_for_component(&ns, &comp); + + // intersection + service_pods.into_iter() + .filter(|pod| component_pods.contains(pod)) + .collect() + } + (Some(service_name), None) => { + //only service + println!("Getting logs for service '{}' in namespace '{}'", service_name, ns); + get_pods_for_service(&ns, &service_name) + } + (None, Some(component_str)) => { + //only component + let comp = Component::from(component_str); + println!("Getting logs for component '{:?}' in namespace '{}'", comp, ns); + get_pods_for_component(&ns, &comp) + } + (None, None) => { + //neither, get all + println!("Getting logs for all pods in namespace '{}'", ns); + get_all_pods(&ns) + } + }; + + if pods.is_empty() { + println!("No pods found matching the specified criteria"); + return; + } + + for pod in pods { + println!("\n{}", "=".repeat(80)); + println!("šŸ“‹ Logs for pod: {}", pod); + println!("{}", "=".repeat(80)); + + let output = Command::new("kubectl") + .args(["logs", &pod, "-n", &ns, "--tail=50"]) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + let stdout = str::from_utf8(&output.stdout).unwrap_or(""); + if stdout.trim().is_empty() { + println!("No logs available for pod '{}'", pod); + } else { + println!("{}", stdout); + } + } else { + let stderr = str::from_utf8(&output.stderr).unwrap_or("Unknown error"); + eprintln!("Error getting logs for pod '{}': {}", pod, stderr); + } + } + Err(err) => { + eprintln!("Failed to execute kubectl logs for pod '{}': {}", pod, err); + } + } + } +} \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index 6fa3d4d..019968c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -4,6 +4,7 @@ mod general; mod uninstall; mod service; mod status; +mod logs; use clap::{ Error, Parser, Subcommand, Args }; use clap::command; @@ -14,6 +15,7 @@ use crate::install::install_cortexflow; use crate::uninstall::uninstall; use crate::service::{list_services, describe_service}; use crate::status::status_command; +use crate::logs::logs_command; use crate::general::GeneralData; @@ -50,6 +52,8 @@ enum Commands { Service(ServiceArgs), #[command(name="status")] Status(StatusArgs), + #[command(name="logs")] + Logs(LogsArgs), } #[derive(Args, Debug, Clone)] struct SetArgs { @@ -85,6 +89,16 @@ struct StatusArgs { namespace: Option, } +#[derive(Args, Debug, Clone)] +struct LogsArgs { + #[arg(long)] + service: Option, + #[arg(long)] + component: Option, + #[arg(long)] + namespace: Option, +} + fn args_parser() -> Result<(), Error> { let args = Cli::parse(); let env = args.env; @@ -131,6 +145,10 @@ fn args_parser() -> Result<(), Error> { status_command(status_args.output, status_args.namespace); Ok(()) } + Some(Commands::Logs(logs_args)) => { + logs_command(logs_args.service, logs_args.component, logs_args.namespace); + Ok(()) + } None => { eprintln!("CLI unknown argument. Cli arguments passed: {:?}", args.cmd); Ok(()) diff --git a/cli/src/mod.rs b/cli/src/mod.rs index 8d3651a..89a4587 100644 --- a/cli/src/mod.rs +++ b/cli/src/mod.rs @@ -3,4 +3,5 @@ pub mod install; pub mod general; pub mod uninstall; pub mod service; -pub mod status; \ No newline at end of file +pub mod status; +pub mod logs; \ No newline at end of file