Skip to content

Commit 70ae38e

Browse files
Merge pull request #108 from PranavVerma-droid/feature/cli
Added `logs` command and integrated it into the CLI structure.
2 parents 1726109 + b28fb47 commit 70ae38e

3 files changed

Lines changed: 216 additions & 1 deletion

File tree

cli/src/logs.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
use std::process::Command;
2+
use std::str;
3+
4+
#[derive(Debug, Clone)]
5+
pub enum Component {
6+
ControlPlane,
7+
DataPlane,
8+
}
9+
10+
impl From<String> for Component {
11+
fn from(s: String) -> Self {
12+
match s.to_lowercase().as_str() {
13+
"control-plane" => Component::ControlPlane,
14+
"data-plane" => Component::DataPlane,
15+
//default will be control plane.
16+
_ => Component::ControlPlane,
17+
}
18+
}
19+
}
20+
21+
impl Component {
22+
fn to_label_selector(&self) -> &str {
23+
match self {
24+
Component::ControlPlane => "component=control-plane",
25+
Component::DataPlane => "component=data-plane",
26+
}
27+
}
28+
}
29+
30+
fn check_namespace_exists(namespace: &str) -> bool {
31+
let output = Command::new("kubectl")
32+
.args(["get", "namespace", namespace])
33+
.output();
34+
35+
match output {
36+
Ok(output) => output.status.success(),
37+
Err(_) => false,
38+
}
39+
}
40+
41+
fn get_available_namespaces() -> Vec<String> {
42+
let output = Command::new("kubectl")
43+
.args(["get", "namespaces", "--no-headers", "-o", "custom-columns=NAME:.metadata.name"])
44+
.output();
45+
46+
match output {
47+
Ok(output) if output.status.success() => {
48+
let stdout = str::from_utf8(&output.stdout).unwrap_or("");
49+
stdout.lines()
50+
.map(|line| line.trim().to_string())
51+
.filter(|line| !line.is_empty())
52+
.collect()
53+
}
54+
_ => Vec::new(),
55+
}
56+
}
57+
58+
fn get_pods_for_service(namespace: &str, service_name: &str) -> Vec<String> {
59+
let output = Command::new("kubectl")
60+
.args(["get", "pods", "-n", namespace, "-l", &format!("app={}", service_name), "--no-headers", "-o", "custom-columns=NAME:.metadata.name"])
61+
.output();
62+
63+
match output {
64+
Ok(output) if output.status.success() => {
65+
let stdout = str::from_utf8(&output.stdout).unwrap_or("");
66+
stdout.lines()
67+
.map(|line| line.trim().to_string())
68+
.filter(|line| !line.is_empty())
69+
.collect()
70+
}
71+
_ => Vec::new(),
72+
}
73+
}
74+
75+
fn get_pods_for_component(namespace: &str, component: &Component) -> Vec<String> {
76+
let output = Command::new("kubectl")
77+
.args(["get", "pods", "-n", namespace, "-l", component.to_label_selector(), "--no-headers", "-o", "custom-columns=NAME:.metadata.name"])
78+
.output();
79+
80+
match output {
81+
Ok(output) if output.status.success() => {
82+
let stdout = str::from_utf8(&output.stdout).unwrap_or("");
83+
stdout.lines()
84+
.map(|line| line.trim().to_string())
85+
.filter(|line| !line.is_empty())
86+
.collect()
87+
}
88+
_ => Vec::new(),
89+
}
90+
}
91+
92+
fn get_all_pods(namespace: &str) -> Vec<String> {
93+
let output = Command::new("kubectl")
94+
.args(["get", "pods", "-n", namespace, "--no-headers", "-o", "custom-columns=NAME:.metadata.name"])
95+
.output();
96+
97+
match output {
98+
Ok(output) if output.status.success() => {
99+
let stdout = str::from_utf8(&output.stdout).unwrap_or("");
100+
stdout.lines()
101+
.map(|line| line.trim().to_string())
102+
.filter(|line| !line.is_empty())
103+
.collect()
104+
}
105+
_ => Vec::new(),
106+
}
107+
}
108+
109+
pub fn logs_command(service: Option<String>, component: Option<String>, namespace: Option<String>) {
110+
let ns = namespace.unwrap_or_else(|| "cortexflow".to_string());
111+
112+
// namespace check
113+
if !check_namespace_exists(&ns) {
114+
let available_namespaces = get_available_namespaces();
115+
116+
println!("\n❌ Namespace '{}' not found", ns);
117+
println!("{}", "=".repeat(50));
118+
119+
if !available_namespaces.is_empty() {
120+
println!("\n📋 Available namespaces:");
121+
for available_ns in &available_namespaces {
122+
println!(" • {}", available_ns);
123+
}
124+
} else {
125+
println!("No namespaces found in the cluster.");
126+
}
127+
128+
std::process::exit(1);
129+
}
130+
131+
// determine pods.
132+
let pods = match (service, component) {
133+
(Some(service_name), Some(component_str)) => {
134+
let comp = Component::from(component_str);
135+
println!("Getting logs for service '{}' with component '{:?}' in namespace '{}'", service_name, comp, ns);
136+
137+
let service_pods = get_pods_for_service(&ns, &service_name);
138+
let component_pods = get_pods_for_component(&ns, &comp);
139+
140+
// intersection
141+
service_pods.into_iter()
142+
.filter(|pod| component_pods.contains(pod))
143+
.collect()
144+
}
145+
(Some(service_name), None) => {
146+
//only service
147+
println!("Getting logs for service '{}' in namespace '{}'", service_name, ns);
148+
get_pods_for_service(&ns, &service_name)
149+
}
150+
(None, Some(component_str)) => {
151+
//only component
152+
let comp = Component::from(component_str);
153+
println!("Getting logs for component '{:?}' in namespace '{}'", comp, ns);
154+
get_pods_for_component(&ns, &comp)
155+
}
156+
(None, None) => {
157+
//neither, get all
158+
println!("Getting logs for all pods in namespace '{}'", ns);
159+
get_all_pods(&ns)
160+
}
161+
};
162+
163+
if pods.is_empty() {
164+
println!("No pods found matching the specified criteria");
165+
return;
166+
}
167+
168+
for pod in pods {
169+
println!("\n{}", "=".repeat(80));
170+
println!("📋 Logs for pod: {}", pod);
171+
println!("{}", "=".repeat(80));
172+
173+
let output = Command::new("kubectl")
174+
.args(["logs", &pod, "-n", &ns, "--tail=50"])
175+
.output();
176+
177+
match output {
178+
Ok(output) => {
179+
if output.status.success() {
180+
let stdout = str::from_utf8(&output.stdout).unwrap_or("");
181+
if stdout.trim().is_empty() {
182+
println!("No logs available for pod '{}'", pod);
183+
} else {
184+
println!("{}", stdout);
185+
}
186+
} else {
187+
let stderr = str::from_utf8(&output.stderr).unwrap_or("Unknown error");
188+
eprintln!("Error getting logs for pod '{}': {}", pod, stderr);
189+
}
190+
}
191+
Err(err) => {
192+
eprintln!("Failed to execute kubectl logs for pod '{}': {}", pod, err);
193+
}
194+
}
195+
}
196+
}

cli/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod general;
44
mod uninstall;
55
mod service;
66
mod status;
7+
mod logs;
78

89
use clap::{ Error, Parser, Subcommand, Args };
910
use clap::command;
@@ -14,6 +15,7 @@ use crate::install::install_cortexflow;
1415
use crate::uninstall::uninstall;
1516
use crate::service::{list_services, describe_service};
1617
use crate::status::status_command;
18+
use crate::logs::logs_command;
1719

1820
use crate::general::GeneralData;
1921

@@ -50,6 +52,8 @@ enum Commands {
5052
Service(ServiceArgs),
5153
#[command(name="status")]
5254
Status(StatusArgs),
55+
#[command(name="logs")]
56+
Logs(LogsArgs),
5357
}
5458
#[derive(Args, Debug, Clone)]
5559
struct SetArgs {
@@ -85,6 +89,16 @@ struct StatusArgs {
8589
namespace: Option<String>,
8690
}
8791

92+
#[derive(Args, Debug, Clone)]
93+
struct LogsArgs {
94+
#[arg(long)]
95+
service: Option<String>,
96+
#[arg(long)]
97+
component: Option<String>,
98+
#[arg(long)]
99+
namespace: Option<String>,
100+
}
101+
88102
fn args_parser() -> Result<(), Error> {
89103
let args = Cli::parse();
90104
let env = args.env;
@@ -131,6 +145,10 @@ fn args_parser() -> Result<(), Error> {
131145
status_command(status_args.output, status_args.namespace);
132146
Ok(())
133147
}
148+
Some(Commands::Logs(logs_args)) => {
149+
logs_command(logs_args.service, logs_args.component, logs_args.namespace);
150+
Ok(())
151+
}
134152
None => {
135153
eprintln!("CLI unknown argument. Cli arguments passed: {:?}", args.cmd);
136154
Ok(())

cli/src/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ pub mod install;
33
pub mod general;
44
pub mod uninstall;
55
pub mod service;
6-
pub mod status;
6+
pub mod status;
7+
pub mod logs;

0 commit comments

Comments
 (0)