diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 67f71b3..6ef922e 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "assert_matches" @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.45" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -380,6 +380,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cortexbrain-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "tracing", + "tracing-subscriber", +] + [[package]] name = "cortexflow-cli" version = "0.1.3" @@ -394,7 +403,6 @@ dependencies = [ "prost", "prost-types", "serde", - "serde_yaml", "tokio", "tonic", "tonic-reflection", @@ -410,6 +418,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "chrono", + "cortexbrain-common", "cortexflow_identity", "prost", "tokio", @@ -433,11 +442,11 @@ dependencies = [ "bytemuck", "bytemuck_derive", "bytes", + "cortexbrain-common", "k8s-openapi", "kube", "libc", "nix", - "serde_yaml", "tokio", "tracing", "tracing-subscriber", @@ -1887,9 +1896,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac5a8627ada0968acec063a4746bf79588aa03ccb66db2f75d7dce26722a40" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", "axum", @@ -1955,9 +1964,9 @@ dependencies = [ [[package]] name = "tonic-reflection" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0267a0073385cd94996197d12acb1856a3a0a2367482c726a48a769f6fed8a3a" +checksum = "34da53e8387581d66db16ff01f98a70b426b091fdf76856e289d5c1bd386ed7b" dependencies = [ "prost", "prost-types", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1af552a..68533ef 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,16 +11,15 @@ license = "Apache-2.0" readme = "../README.md" [dependencies] -clap = { version = "4.5.38", features = ["derive"] } +clap = { version = "4.5.51", features = ["derive"] } colored = "3.0.0" directories = "6.0.0" serde = { version = "1.0.219", features = ["derive"] } -serde_yaml = "0.9.34" tracing = "0.1.41" tokio = {version = "1.47.0",features = ["macros",'rt-multi-thread']} -anyhow = "1.0.98" -tonic = "0.14.1" -tonic-reflection = "0.14.1" +anyhow = "1.0.100" +tonic = "0.14.2" +tonic-reflection = "0.14.2" prost-types = "0.14.1" prost = "0.14.1" cortexflow_agent_api = {path = "../core/api",features = ["client"]} diff --git a/cli/src/essential.rs b/cli/src/essential.rs index 37f0e5d..1a4d44f 100644 --- a/cli/src/essential.rs +++ b/cli/src/essential.rs @@ -1,89 +1,146 @@ -use std::collections::BTreeMap; -use std::ptr::read; -//TODO: Check if is possible to use the get_config_path function. Check for reusable components -use std::{fs, io::stdin, path::PathBuf, process::exit}; +use std::{ collections::BTreeMap, fmt, process::Command, result::Result::Ok }; -use directories::ProjectDirs; -use k8s_openapi::api::core::v1::ConfigMap; -use k8s_openapi::serde_json::json; -use kube::Config; -use prost_types::MethodDescriptorProto; +use kube::core::ErrorResponse; use serde::Serialize; -use std::fs::{Metadata, OpenOptions}; -use std::result::Result::Ok; - use colored::Colorize; -use std::thread; -use std::time::Duration; - -use std::process::Command; -use kube::api::{Api, ObjectMeta, Patch, PatchParams, PostParams}; +use k8s_openapi::api::core::v1::ConfigMap; +use k8s_openapi::serde_json::json; +use kube::api::{ Api, ObjectMeta, Patch, PatchParams, PostParams }; use kube::client::Client; -pub struct GeneralData { - env: String, -} -#[derive(Serialize)] -pub struct MetadataConfigFile { - blocklist: Vec, -} +pub static BASE_COMMAND: &str = "kubectl"; // docs: Kubernetes base command + +// docs: +// +// CliError enum to group all the errors +// +// Custom error definition +// InstallerError: +// - used for general installation errors occured during the installation of cortexflow components. Can be used for: +// - Return downloading errors +// - Return unsuccessful file removal during installation +// +// ClientError: +// - used for Kubernetes client errors. Can be used for: +// - Return client connection errors +// +// UninstallError: +// - used for general installation errors occured during the uninstall for cortexflow components. Can be used for: +// - Return components removal errors +// +// AgentError: +// - used for cortexflow agent errors. Can be used for: +// - return errors from the reflection server +// - return unavailable agent errors (404) +// +// MonitoringError: +// - used for general monitoring errors. TODO: currently under implementation +// +// implements fmt::Display for user friendly error messages + #[derive(Debug)] -pub enum Environments { - Kubernetes, +pub enum CliError { + InstallerError { + reason: String, + }, + ClientError(kube::Error), + UninstallError { + reason: String, + }, + AgentError(tonic_reflection::server::Error), + MonitoringError { + reason: String, + }, } -impl TryFrom<&str> for Environments { - type Error = String; - - fn try_from(environment: &str) -> Result { - match environment { - "kubernetes" | "k8s" => Ok(Environments::Kubernetes), - _ => - Err( - format!("Environment '{}' not supported. Please insert a supported value: Kubernetes, K8s", environment) - ), - } +// docs: +// error type conversions + +impl From for CliError { + fn from(e: kube::Error) -> Self { + CliError::ClientError(e) } } - -//for owned types -impl TryFrom for Environments { - type Error = String; - - fn try_from(environment: String) -> Result { - Environments::try_from(environment.as_str()) +impl From for CliError { + fn from(e: anyhow::Error) -> Self { + CliError::MonitoringError { reason: format!("{}", e) } } } +impl From<()> for CliError { + fn from (v: ()) -> Self{ + return ().into() + } +} + +// docs: +// fmt::Display implementation for CliError type. Creates a user friendly message error message. +// TODO: implement colored messages using the colorize crate for better output display -impl Environments { - pub fn base_command(&self) -> &'static str { +impl fmt::Display for CliError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Environments::Kubernetes => "kubectl", + CliError::InstallerError { reason } => { + write!( + f, + "An error occured while installing cortexflow components. Reason: {}", + reason + ) + } + CliError::UninstallError { reason } => { + write!( + f, + "An error occured while installing cortexflow components. Reason: {}", + reason + ) + } + CliError::MonitoringError { reason } => { + write!( + f, + "An error occured while installing cortexflow components. Reason: {}", + reason + ) + } + CliError::ClientError(e) => write!(f, "Client Error: {}", e), + CliError::AgentError(e) => write!(f, "Agent Error: {}", e), } } } -impl GeneralData { - pub const VERSION: &str = env!("CARGO_PKG_VERSION"); - pub const AUTHOR: &str = env!("CARGO_PKG_AUTHORS"); - pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); +#[derive(Serialize)] +pub struct MetadataConfigFile { + blocklist: Vec, +} - pub fn new(env: String) -> Self { - GeneralData { - env: env.to_string(), - } - } - pub fn set_env(mut self, env: String) { - self.env = env; - } - pub fn get_env(self) -> String { - self.env - } - pub fn get_env_output(self) { - println!("{:?}", self.env) - } +// docs: +// +// This is a wrapper functions used to create a kubernetes client session +// Used in modules: +// - essentials +// - install +// - logs +// - service +// - status +// - uninstall +// +// +// Returns a Result with the client an a kube::Error + +pub async fn connect_to_client() -> Result { + let client = Client::try_default().await; + client } +// docs: +// +// This is an function used to update the cli +// +// Steps: +// - Checks the current CLI version +// - if the version matches the current latest version doesn't do anything +// - else runs the cargo update command +// +// Returns an error if the command fails + pub fn update_cli() { println!("{} {}", "=====>".blue().bold(), "Updating CortexFlow CLI"); println!("{} {}", "=====>".blue().bold(), "Looking for a newer version"); @@ -96,17 +153,21 @@ pub fn update_cli() { println!("āœ… Updated CLI"); } } -pub fn info(general_data: GeneralData) { - println!("{} {} {}", "=====>".blue().bold(), "Version:", GeneralData::VERSION); - println!("{} {} {}", "=====>".blue().bold(), "Author:", GeneralData::AUTHOR); - println!("{} {} {}", "=====>".blue().bold(), "Description:", GeneralData::DESCRIPTION); - println!("{} {} {}", "=====>".blue().bold(), "Environment:", general_data.get_env()); -} -fn is_supported_env(env: &str) -> bool { - matches!(env.to_lowercase().trim(), "kubernetes" | "k8s") +// docs: +// +// This is a function to display the CLI Version,Author and Description using a fancy output style + +pub fn info() { + println!("{} {} {}", "=====>".blue().bold(), "Version:", env!("CARGO_PKG_VERSION")); + println!("{} {} {}", "=====>".blue().bold(), "Author:", env!("CARGO_PKG_AUTHORS")); + println!("{} {} {}", "=====>".blue().bold(), "Description:", env!("CARGO_PKG_DESCRIPTION")); } +// docs: +// +// This is a wrapper function to create the MetadataConfigFile structure + pub fn create_configs() -> MetadataConfigFile { let mut blocklist: Vec = Vec::new(); blocklist.push("".to_string()); @@ -114,63 +175,138 @@ pub fn create_configs() -> MetadataConfigFile { let configs = MetadataConfigFile { blocklist }; configs } -pub async fn read_configs() -> Result, anyhow::Error> { - let client = Client::try_default().await?; - let namespace = "cortexflow"; - let configmap = "cortexbrain-client-config"; - let api: Api = Api::namespaced(client, namespace); - - let cm = api.get(configmap).await?; - - if let Some(data) = cm.data { - if let Some(blocklist_raw) = data.get("blocklist") { - let lines: Vec = blocklist_raw - .lines() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) // ignora righe vuote - .collect(); - - return Ok(lines); + +// docs: +// +// This is an helper functions used to read the configs from a kubernetes configmap +// +// Steps: +// - connects to kubernetes client +// - read the configmap from the kubernetes API. Needed: namespace_name , configmap_name +// - returns the given configmap blocklist data in a Vec type +// +// Returns an error if something fails + +pub async fn read_configs() -> Result, CliError> { + match connect_to_client().await { + Ok(client) => { + let namespace = "cortexflow"; + let configmap = "cortexbrain-client-config"; + let api: Api = Api::namespaced(client, namespace); + + let cm = api.get(configmap).await?; + + if let Some(data) = cm.data { + if let Some(blocklist_raw) = data.get("blocklist") { + let lines: Vec = blocklist_raw + .lines() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) // ignore empty lines + .collect(); + + return Ok(lines); + } + } + + Ok(Vec::new()) //in case the key fails + } + Err(_) => { + Err( + CliError::ClientError( + kube::Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } } - - Ok(Vec::new()) //in case the key fails } -pub async fn create_config_file(config_struct: MetadataConfigFile) -> Result<(), anyhow::Error> { - let client = Client::try_default().await?; - let namespace = "cortexflow"; - let configmap = "cortexbrain-client-config"; - - let api: Api = Api::namespaced(client, namespace); - // create configmap - let mut data = BTreeMap::new(); - for x in config_struct.blocklist { - data.insert("blocklist".to_string(), x); - } - let cm = ConfigMap { - metadata: ObjectMeta { - name: Some("cortexbrain-client-config".to_string()), - ..Default::default() - }, // type ObjectMeta - data: Some(data), //type Option> - ..Default::default() - }; - match api.create(&PostParams::default(), &cm).await { - Ok(_) => { - println!("Configmap created successfully"); +// docs: +// +// This is a function used to create a configmap file +// +// With the version 0.1.4 cortexflow introduced a configmap file to store the relevant cortexflow metadata +// Up to now the metadata includes: +// - blocked ip addresses passed using the CLI +// +// Steps: +// - connects to kubernetes client +// - creates a configmap named "cortexbrain-client-config" stored in the cortexflow namespace +// - the blocklist field is initialized with zero blocked addresses +// +// Returns an error if something fails + +pub async fn create_config_file(config_struct: MetadataConfigFile) -> Result<(), CliError> { + match connect_to_client().await { + Ok(client) => { + let namespace = "cortexflow"; + let configmap = "cortexbrain-client-config"; + + let api: Api = Api::namespaced(client, namespace); + + // create configmap + let mut data = BTreeMap::new(); + for x in config_struct.blocklist { + data.insert("blocklist".to_string(), x); + } + let cm = ConfigMap { + metadata: ObjectMeta { + name: Some("cortexbrain-client-config".to_string()), + ..Default::default() + }, // type ObjectMeta + data: Some(data), //type Option> + ..Default::default() + }; + match api.create(&PostParams::default(), &cm).await { + Ok(_) => { + println!("Configmap created successfully"); + } + Err(e) => { + eprintln!("An error occured: {}", e); + } + } + Ok(()) } - Err(e) => { - eprintln!("An error occured: {}", e); + Err(_) => { + Err( + CliError::ClientError( + kube::Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } - }; - Ok(()) + } } -pub async fn update_config_metadata(input: &str, action: &str) { +// docs: +// +// This is a function used to update a configmap file. Takes an input and an action +// +// Input: an ip (&str type) +// Actions: +// - Add: add the ip to the blocklist metadata +// - Delete: remove the ip from the blocklist metadata +// +// Steps: +// - connects to kubernetes client +// - reads the existing configmap +// - creates a temporary vector with the old addresses and the new address +// - creates a patch by calling the update_configamp file +// +// Returns an error if something fails + +pub async fn update_config_metadata(input: &str, action: &str) -> Result<(), CliError> { if action == "add" { //retrieve current blocked ips list - let mut ips = read_configs().await.unwrap(); + let mut ips = read_configs().await?; println!("Readed current blocked ips: {:?}", ips); //create a temporary vector of ips @@ -179,9 +315,9 @@ pub async fn update_config_metadata(input: &str, action: &str) { // override blocklist parameters let new_configs = MetadataConfigFile { blocklist: ips }; //create a new config - update_configmap(new_configs).await; + update_configmap(new_configs).await?; } else if action == "delete" { - let mut ips = read_configs().await.unwrap(); + let mut ips = read_configs().await?; if let Some(index) = ips.iter().position(|target| target == &input.to_string()) { ips.remove(index); } else { @@ -191,61 +327,69 @@ pub async fn update_config_metadata(input: &str, action: &str) { // override blocklist parameters let new_configs = MetadataConfigFile { blocklist: ips }; //create a new config - update_configmap(new_configs).await; + update_configmap(new_configs).await?; } + Ok(()) } -pub async fn update_configmap(config_struct: MetadataConfigFile) -> Result<(), anyhow::Error> { - let client = Client::try_default().await?; - let namespace = "cortexflow"; - let name = "cortexbrain-client-config"; - let api: Api = Api::namespaced(client, namespace); - - let blocklist_yaml = config_struct - .blocklist - .iter() - .map(|x| format!("{}", x)) - .collect::>() - .join("\n"); - - let patch = Patch::Apply(json!({ - "apiVersion": "v1", - "kind": "ConfigMap", - "data": { - "blocklist": blocklist_yaml +// docs: +// +// This is a function used to create a patch to update a configmap +// +// Steps: +// - connects to kubernetes client +// - creates a patch using the config_struct data +// - pushes the patch to the kubernetes API +// +// Returns an error if something fails + +pub async fn update_configmap(config_struct: MetadataConfigFile) -> Result<(), CliError> { + match connect_to_client().await { + Ok(client) => { + let namespace = "cortexflow"; + let name = "cortexbrain-client-config"; + let api: Api = Api::namespaced(client, namespace); + + let blocklist_yaml = config_struct.blocklist + .iter() + .map(|x| format!("{}", x)) + .collect::>() + .join("\n"); + + let patch = Patch::Apply( + json!({ + "apiVersion": "v1", + "kind": "ConfigMap", + "data": { + "blocklist": blocklist_yaml + } + }) + ); + + let patch_params = PatchParams::apply("cortexbrain").force(); + match api.patch(name, &patch_params, &patch).await { + Ok(_) => { + println!("Map updated successfully"); + } + Err(e) => { + eprintln!("An error occured during the patching process: {}", e); + return Err(e.into()); + } + } + + Ok(()) } - })); - - let patch_params = PatchParams::apply("cortexbrain").force(); - match api.patch(name, &patch_params, &patch).await { - Ok(_) => { - println!("Map updated successfully"); - } - Err(e) => { - eprintln!("An error occured during the patching process: {}", e); - return Err(e.into()); + Err(_) => { + Err( + CliError::ClientError( + kube::Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } } - - Ok(()) -} - -//TODO: add here an explanation of what are config_dir and file_path -pub fn get_config_directory() -> Result<(PathBuf, PathBuf), ()> { - let dirs = ProjectDirs::from("org", "cortexflow", "cfcli").expect( - "Cannot determine the config directory" - ); - let config_dir = dirs.config_dir().to_path_buf(); - let file_path = config_dir.join("config.yaml"); - - Ok((config_dir, file_path)) -} - -pub fn get_startup_config_dir() -> bool { - ProjectDirs::from("org", "cortexflow", "cfcli") - .map(|dirs| { - let path = dirs.config_dir(); - path.exists() - }) - .unwrap_or(false) } diff --git a/cli/src/install.rs b/cli/src/install.rs index af0ae48..644212d 100644 --- a/cli/src/install.rs +++ b/cli/src/install.rs @@ -1,17 +1,32 @@ -use std::process::{ Command, exit }; +use colored::Colorize; +use kube::{ core::ErrorResponse }; +use clap::{ Args, Subcommand, command }; +use std::{ process::{ Command }, thread, time::Duration }; +use crate::{ + essential::{ connect_to_client, create_config_file, create_configs, BASE_COMMAND, CliError }, +}; +use kube::Error; -use crate::essential::Environments; -use crate::essential::{ create_config_file, create_configs, get_config_directory, read_configs }; +// docs: +// +// Custom enum definition: +// InstallationType: +// - used to pass installation files. Can be used for: +// - Install components by passing a Vec containing the components urls +// - Install a simple-example by passing the component url (String) +// +// -use colored::Colorize; -use std::thread; -use std::time::Duration; -use tracing::debug; +enum InstallationType { + Components(Vec), + SimpleExample(String), +} -use clap::command; -use clap::{ Args, Subcommand }; +// docs: +// +// main cortexflow installation function to install all the cortexflow components: +// This function creates the cortexflow namespace, manages the metadata file creation and removes the temporary installation files -//install subcommands #[derive(Subcommand, Debug, Clone)] pub enum InstallCommands { #[command(name = "cortexflow", about = "Install all the CortexBrain core components")] @@ -30,205 +45,321 @@ pub struct InstallArgs { pub install_cmd: InstallCommands, } -/* components installation function */ -fn install_cluster_components(env: String) { - let user_env = Environments::try_from(env.to_lowercase()); - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); +// docs: +// +// main cortexflow installation function to install all the cortexflow components: +// This function creates the cortexflow namespace, manages the metadata file creation and removes the temporary installation files + +pub async fn install_cortexflow() -> Result<(), CliError> { + println!("{} {}", "=====>".blue().bold(), "Preparing cortexflow installation".white()); + println!("{} {}", "=====>".blue().bold(), "Creating the config files".white()); + println!("{} {}", "=====>".blue().bold(), "Creating cortexflow namespace".white()); + Command::new("kubectl") + .args(["create", "namespace", "cortexflow"]) + .output() + .expect("Failed to create cortexflow namespace"); + + let metadata_configs = create_configs(); + create_config_file(metadata_configs).await?; + install_cluster_components().await?; + Ok(()) +} + +// docs: +// +// main cortexflow installation function to install the examples: +// This function installs the demostration examples + +pub async fn install_simple_example() -> Result<(), CliError> { + println!("{} {}", "=====>".blue().bold(), "Installing simple example".white()); + install_simple_example_component().await?; + Ok(()) +} + +//docs: +// +// This function manages the installation of the cortexflow cluster components +// Steps: +// - Connects to kubernetes client +// - Copies installation files from the offcial github repository +// - Executes the install_components function +// - Executes the rm_installation_files to remove the temporary installation files +// +// Returns an CliError if something fails + +async fn install_cluster_components() -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { println!("{} {}", "=====>".blue().bold(), "Copying installation files".white()); - copy_installation_files(); + download_installation_files( + InstallationType::Components( + vec![ + "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/main/core/src/testing/configmap-role.yaml".to_string(), + "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/main/core/src/testing/rolebinding.yaml".to_string(), + "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/main/core/src/testing/cortexflow-rolebinding.yaml".to_string(), + "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/feature/ebpf-core/core/src/testing/identity.yaml".to_string(), + "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/feature/ebpf-core/core/src/testing/agent.yaml".to_string() + ] + ) + )?; thread::sleep(Duration::from_secs(1)); - install_components(env.to_string()); + install_components("cortexbrain")?; println!("\n"); - rm_installation_files(); + rm_installation_files( + InstallationType::Components( + vec![ + "configmap-role.yaml".to_string(), + "rolebinding.yaml".to_string(), + "cortexflow-rolebinding.yaml".to_string(), + "identity.yaml".to_string(), + "agent.yaml".to_string() + ] + ) + )?; println!("{} {}", "=====>".blue().bold(), "installation completed".white()); + Ok(()) } Err(e) => { - eprintln!("An error occured while installing cortexflow components: {:?}", e); - exit(1) + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } } } -/* example installation function */ -fn install_simple_example_component(env: String) { - let user_env = Environments::try_from(env.to_lowercase()); - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); +//docs: +// +// This function manages the installation of the examples +// Steps: +// - Connects to kubernetes client +// - Copies examples files from the offcial github repository +// - Executes the install_example function +// - Executes the rm_example_installation_file to remove the temporary installation files +// +// Returns an CliError if something fails + +async fn install_simple_example_component() -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { println!("{} {}", "=====>".blue().bold(), "Copying installation files".white()); - copy_example_installation_file(); + download_installation_files( + InstallationType::SimpleExample( + "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/feature/ebpf-core/core/src/testing/deploy-test-pod.yaml".to_string() + ) + )?; thread::sleep(Duration::from_secs(1)); - install_example(env.to_string()); + install_components("simple-example")?; println!("\n"); - rm_example_installation_file(); + rm_installation_files( + InstallationType::SimpleExample("deploy-test-pod.yaml".to_string()) + )?; println!("{} {}", "=====>".blue().bold(), "installation completed".white()); + Ok(()) } Err(e) => { - eprintln!("An error occured while installing cortexflow components: {:?}", e); - exit(1) + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } } } -/* main installation function */ -pub async fn install_cortexflow() { - println!("{} {}", "=====>".blue().bold(), "Preparing cortexflow installation".white()); - println!("{} {}", "=====>".blue().bold(), "Creating the config files".white()); - println!("{} {}", "=====>".blue().bold(), "Creating cortexflow namespace".white()); - Command::new("kubectl") - .args(["create", "namespace", "cortexflow"]) - .output() - .expect("Failed to create cortexflow namespace"); - - let metadata_configs = create_configs(); - create_config_file(metadata_configs).await; +//docs: +// +// This is an auxiliary function to help manage the cortexflow components during the installation +// Steps: +// - Read the Vec<&str> with the list of components to install +// - Executes the apply_component function +// - let env = "kubernetes".to_string(); - install_cluster_components(env); -} -/* install simple example */ -pub fn install_simple_example() { - println!("{} {}", "=====>".blue().bold(), "Installing simple example".white()); +fn install_components(components_type: &str) -> Result<(), CliError> { + if components_type == "cortexbrain" { + let files_to_install = vec![ + "configmap-role.yaml", + "rolebinding.yaml", + "cortexflow-rolebinding.yaml", + "identity.yaml", + "agent.yaml" + ]; + let tot_files = files_to_install.len(); - let file_path = get_config_directory().unwrap().1; + println!("{} {}", "=====>".blue().bold(), "Installing cortexflow components".white()); + let mut i = 1; - let env = "kubectl".to_string(); - install_simple_example_component(env); -} + for component in files_to_install { + println!( + "{} {}{}{}{} {} {} {}", + "=====>".blue().bold(), + "(", + i, + "/", + tot_files, + ")", + "Applying ", + component + ); + apply_component(component); + i = i + 1; + } + } else if components_type == "simple-example" { + let files_to_install = vec!["deploy-test-pod.yaml"]; + let tot_files = files_to_install.len(); + let mut i = 1; -/* install example component */ -fn install_example(env: String) { - let files_to_install = vec!["deploy-test-pod.yaml"]; - let tot_files = files_to_install.len(); - - println!("{} {}", "=====>".blue().bold(), "Installing cortexflow components".white()); - let user_env = env.as_str(); - debug!("Debugging env var in install components {:?}", user_env); - - let mut i = 1; - - for component in files_to_install { - println!( - "{} {}{}{}{} {} {} {}", - "=====>".blue().bold(), - "(", - i, - "/", - tot_files, - ")", - "Applying ", - component - ); - apply_component(component, user_env); - i = i + 1; + for component in files_to_install { + println!( + "{} {}{}{}{} {} {} {}", + "=====>".blue().bold(), + "(", + i, + "/", + tot_files, + ")", + "Applying ", + component + ); + apply_component(component); + i = i + 1; + } + } else { + return Err(CliError::InstallerError { + reason: "No installation type selected".to_string(), + }); } + Ok(()) } -/* Installation functions */ -fn install_components(env: String) { - let files_to_install = vec![ - "configmap-role.yaml", - "rolebinding.yaml", - "cortexflow-rolebinding.yaml", - "identity.yaml", - "agent.yaml" - ]; - let tot_files = files_to_install.len(); - - println!("{} {}", "=====>".blue().bold(), "Installing cortexflow components".white()); - let user_env = env.as_str(); - debug!("Debugging env var in install components {:?}", user_env); - - let mut i = 1; - - for component in files_to_install { - println!( - "{} {}{}{}{} {} {} {}", - "=====>".blue().bold(), - "(", - i, - "/", - tot_files, - ")", - "Applying ", - component - ); - apply_component(component, user_env); - i = i + 1; - } -} +//docs: +// +// This is an auxiliary function to help manage the cortexflow components during the installation +// Steps: +// - Read the file name of a kubernetes manifest (e.g agent.yaml) +// - Applies the manifest using the command kubectl apply -f +// +// Returns an CliError if something fails -fn apply_component(file: &str, env: &str) { - let output = Command::new(env) +fn apply_component(file: &str) -> Result<(), CliError> { + let output = Command::new(BASE_COMMAND) .args(["apply", "-f", file]) .output() - .expect("cannot install component from file"); + .map_err(|_| CliError::InstallerError { + reason: "Can't install component from file".to_string(), + })?; if !output.status.success() { eprintln!("Error installing file: {}:\n{}", file, String::from_utf8_lossy(&output.stderr)); } else { println!("āœ… Applied {}", file); } - thread::sleep(Duration::from_secs(2)); + Ok(()) } -fn copy_installation_files() { - download_file( - "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/main/core/src/testing/configmap-role.yaml" - ); - download_file( - "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/main/core/src/testing/rolebinding.yaml" - ); - download_file( - "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/main/core/src/testing/cortexflow-rolebinding.yaml" - ); - download_file( - "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/feature/ebpf-core/core/src/testing/identity.yaml" - ); - download_file( - "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/feature/ebpf-core/core/src/testing/agent.yaml" - ); - println!("\n"); -} -fn copy_example_installation_file() { - download_file( - "https://raw.githubusercontent.com/CortexFlow/CortexBrain/refs/heads/feature/ebpf-core/core/src/testing/deploy-test-pod.yaml" - ); +//docs: +// +// This is an auxiliary function to download all the installation files +// Steps: +// - Read the Vec containing the file names of the installation files from the InstallationType enum +// - Download the corresponding installation files from the github repository +// +// Returns an CliError if something fails + +fn download_installation_files(installation_files: InstallationType) -> Result<(), CliError> { + match installation_files { + InstallationType::Components(files) => { + for src in files.iter() { + download_file(&src)?; + } + } + InstallationType::SimpleExample(file) => { + download_file(&file)?; + } + } println!("\n"); + Ok(()) } -fn rm_installation_files() { - println!("{} {}", "=====>".blue().bold(), "Removing temporary installation files".white()); - rm_file("configmap-role.yaml"); - rm_file("rolebinding.yaml"); - rm_file("cortexflow-rolebinding.yaml"); - rm_file("identity.yaml"); - rm_file("agent.yaml"); -} -fn rm_example_installation_file() { + +//docs: +// +// This is an auxiliary function to specifically remove the installation files after the installation +// Steps: +// - Read the Vec containing the file names of the installation files from the InstallationType enum +// - Executes the rm_file function for each installation file +// +// Returns an CliError if something fails + +fn rm_installation_files(file_to_remove: InstallationType) -> Result<(), CliError> { println!("{} {}", "=====>".blue().bold(), "Removing temporary installation files".white()); - rm_file("deploy-test-pod.yaml"); + match file_to_remove { + InstallationType::Components(files) => { + for src in files.iter() { + rm_file(&src)?; + } + } + InstallationType::SimpleExample(file) => { + rm_file(&file)?; + } + } + + Ok(()) } -/* Auxiliary functions */ -fn download_file(src: &str) { - let output = Command::new("wget").args([src]).output().expect("cannot import config file"); +//docs: +// +// This is an auxiliary function to help manage the cortexflow components during the installation +// Steps: +// - Read the url name of a kubernetes manifest +// - Download the manifest file from the cortexflow repository +// +// Returns a CliError if something fails + +fn download_file(src: &str) -> Result<(), CliError> { + let output = Command::new("wget") + .args([src]) + .output() + .map_err(|_| CliError::InstallerError { + reason: "An error occured: component download failed".to_string(), + })?; if !output.status.success() { eprintln!("Error copying file: {}.\n{}", src, String::from_utf8_lossy(&output.stderr)); } else { println!("āœ… Copied file from {} ", src); } - thread::sleep(Duration::from_secs(2)); + Ok(()) } -fn rm_file(file_to_remove: &str) { + +//docs: +// +// This is an auxiliary function to help manage the cortexflow components during the installation +// Steps: +// - Read the file name +// - Removes the file using the rm -f +// +// Returns an CliError if something fails + +fn rm_file(file_to_remove: &str) -> Result<(), CliError> { let output = Command::new("rm") .args(["-f", file_to_remove]) .output() - .expect("cannot remove temporary installation file"); + .map_err(|_| CliError::InstallerError { + reason: "cannot remove temporary installation file".to_string(), + })?; if !output.status.success() { eprintln!( @@ -241,4 +372,5 @@ fn rm_file(file_to_remove: &str) { } thread::sleep(Duration::from_secs(2)); + Ok(()) } diff --git a/cli/src/logs.rs b/cli/src/logs.rs index 2e760d3..bd819cc 100644 --- a/cli/src/logs.rs +++ b/cli/src/logs.rs @@ -1,12 +1,8 @@ -use std::str; -use std::process::Command; - +use std::{ str, process::Command, result::Result::Ok }; use colored::Colorize; - -use crate::essential::{Environments, get_config_directory, read_configs}; - use clap::Args; - +use kube::{ Error, core::ErrorResponse }; +use crate::essential::{ connect_to_client, BASE_COMMAND, CliError }; #[derive(Args, Debug, Clone)] pub struct LogsArgs { @@ -18,7 +14,6 @@ pub struct LogsArgs { pub namespace: Option, } - #[derive(Debug, Clone)] pub enum Component { ControlPlane, @@ -45,38 +40,176 @@ impl Component { } } -fn check_namespace_exists(namespace: &str) -> bool { - let file_path = get_config_directory().unwrap().1; +// docs: +// +// This is the main function for the logs command +// Steps: +// - connects to kubernetes client +// - returns the list of namespaces in Vec format +// +// +// Returns a CliError if the connectiion to the kubeapi fails + +pub async fn logs_command( + service: Option, + component: Option, + namespace: Option +) -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { + let ns = namespace.unwrap_or_else(|| "cortexflow".to_string()); - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); + if !check_namespace_exists(&ns).await? { + let available_namespaces = get_available_namespaces().await?; + 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); + } - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) - .args(["get", "namespace", namespace]) - .output(); + 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 '{}'", + "=====>".blue().bold(), + service_name, + comp, + ns + ); + let service_pods = get_pods_for_service(&ns, &service_name).await?; + let component_pods = get_pods_for_component(&ns, &comp).await?; + service_pods + .into_iter() + .filter(|pod| component_pods.contains(pod)) + .collect() + } + (Some(service_name), None) => { + println!("Getting logs for service '{}' in namespace '{}'", service_name, ns); + get_pods_for_service(&ns, &service_name).await? + } + (None, Some(component_str)) => { + let comp = Component::from(component_str); + println!("Getting logs for component '{:?}' in namespace '{}'", comp, ns); + get_pods_for_component(&ns, &comp).await? + } + (None, None) => { + println!( + "{} Getting logs for all pods in namespace '{}'", + "=====>".blue().bold(), + ns + ); + get_all_pods(&ns).await? + } + }; - match output { - Ok(output) => output.status.success(), - Err(_) => false, + if pods.is_empty() { + println!("No pods found matching the specified criteria"); + return Ok(()); } + + for pod in pods { + println!("{} Logs for pod: {:?}", "=====>".blue().bold(), pod); + match + Command::new(BASE_COMMAND).args(["logs", &pod, "-n", &ns, "--tail=50"]).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 {} logs for pod '{:?}': {}", + BASE_COMMAND, + pod, + err + ); + } + } + } + + Ok(()) + } + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } - Err(_) => false, } } -fn get_available_namespaces() -> Vec { - let file_path = get_config_directory().unwrap().1; +// docs: +// +// This is an auxiliary function used in the logs_command +// Steps: +// - connects to kubernetes client +// - returns true if the namespace exists or false if the namespace doesn't exists +// +// +// Returns a CliError if the connection fails + +pub async fn check_namespace_exists(namespace: &str) -> Result { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND).args(["get", "namespace", namespace]).output(); - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); + match output { + Ok(output) => Ok(output.status.success()), + Err(_) => Ok(false), + } + } + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } + } +} - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) +// docs: +// +// This function returns the available namespaces: +// Steps: +// - connects to kubernetes client +// - returns the list of namespaces in Vec format +// +// +// Returns a CliError if the connectiion to the kubeapi fails + +pub async fn get_available_namespaces() -> Result, CliError> { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND) .args([ "get", "namespaces", @@ -89,29 +222,48 @@ fn get_available_namespaces() -> Vec { match output { Ok(output) if output.status.success() => { let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - stdout + let ns = stdout .lines() .map(|line| line.trim().to_string()) .filter(|line| !line.is_empty()) - .collect() + .collect(); + Ok(ns) } - _ => Vec::new(), + _ => Ok(Vec::new()), } } - Err(_) => Vec::new(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } -fn get_pods_for_service(namespace: &str, service_name: &str) -> Vec { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) +// docs: +// +// This function returns the pods: +// Steps: +// - connects to kubernetes client +// - returns the list of pods associated with a kubernetes service filtering by labels in Vec format +// +// +// Returns a CliError if the connectiion to the kubeapi fails + +async fn get_pods_for_service( + namespace: &str, + service_name: &str +) -> Result, CliError> { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND) .args([ "get", "pods", @@ -128,29 +280,49 @@ fn get_pods_for_service(namespace: &str, service_name: &str) -> Vec { match output { Ok(output) if output.status.success() => { let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - stdout + let pods = stdout .lines() .map(|line| line.trim().to_string()) .filter(|line| !line.is_empty()) - .collect() + .collect(); + Ok(pods) } - _ => Vec::new(), + _ => Ok(Vec::new()), } } - Err(_) => Vec::new(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } -fn get_pods_for_component(namespace: &str, component: &Component) -> Vec { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) +// docs: +// +// This function returns the pods: +// Steps: +// - connects to kubernetes client +// - returns the list of pods associated with a componet object to dynamically construct the +// label selector,in Vec format +// +// +// Returns a CliError if the connectiion to the kubeapi fails + +async fn get_pods_for_component( + namespace: &str, + component: &Component +) -> Result, CliError> { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND) .args([ "get", "pods", @@ -167,29 +339,45 @@ fn get_pods_for_component(namespace: &str, component: &Component) -> Vec match output { Ok(output) if output.status.success() => { let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - stdout + let pods = stdout .lines() .map(|line| line.trim().to_string()) .filter(|line| !line.is_empty()) - .collect() + .collect(); + Ok(pods) } - _ => Vec::new(), + _ => Ok(Vec::new()), } } - Err(_) => Vec::new(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } -fn get_all_pods(namespace: &str) -> Vec { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) +// docs: +// +// This function returns the available namespaces: +// Steps: +// - connects to kubernetes client +// - returns the list of all pods in Vec format +// +// +// Returns a CliError if the connectiion to the kubeapi fails + +async fn get_all_pods(namespace: &str) -> Result, CliError> { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND) .args([ "get", "pods", @@ -204,134 +392,27 @@ fn get_all_pods(namespace: &str) -> Vec { match output { Ok(output) if output.status.success() => { let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - stdout + let pods = stdout .lines() .map(|line| line.trim().to_string()) .filter(|line| !line.is_empty()) - .collect() + .collect(); + Ok(pods) } - _ => Vec::new(), + _ => Ok(Vec::new()), } } - Err(_) => Vec::new(), - } -} - -pub fn logs_command(service: Option, component: Option, namespace: Option) { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - 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!( - "{} {} {} {} {} {:?} {}", - "=====>".blue().bold(), - "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!( - "{} {} {}", - "=====>".blue().bold(), - "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!("{} {} {}", "=====>".blue().bold(), "Logs for pod: ", pod); - - let output = Command::new(env) - .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 {} logs for pod '{}': {}", env, pod, err); - } - } - } + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } - Err(e) => eprintln!("An error occured while returning the cluster environment: {:?}",e), } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 77f1104..fb49d09 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1,4 @@ #![allow(warnings)] -//TODO: add an example with test pods during installation mod essential; mod install; mod logs; @@ -9,31 +8,34 @@ mod service; mod status; mod uninstall; -use clap::command; -use clap::{Args, Error, Parser, Subcommand}; +use clap::{ Args, Parser, Subcommand }; use colored::Colorize; use std::result::Result::Ok; -use std::string; use tracing::debug; -use crate::essential::{get_config_directory, get_startup_config_dir, info, read_configs, update_cli}; -use crate::install::{InstallArgs, InstallCommands, install_cortexflow, install_simple_example}; -use crate::logs::{LogsArgs, logs_command}; -use crate::monitoring::{MonitorArgs, MonitorCommands, list_features, monitor_identity_events}; -use crate::policies::{PoliciesArgs, PoliciesCommands, check_blocklist, create_blocklist, remove_ip}; -use crate::service::{ServiceArgs, ServiceCommands, describe_service, list_services}; -use crate::status::{StatusArgs, status_command}; +use crate::essential::{ CliError, info, update_cli }; +use crate::install::{ InstallArgs, InstallCommands, install_cortexflow, install_simple_example }; +use crate::logs::{ LogsArgs, logs_command }; +use crate::monitoring::{ MonitorArgs, MonitorCommands, list_features, monitor_identity_events }; +use crate::policies::{ + PoliciesArgs, + PoliciesCommands, + check_blocklist, + create_blocklist, + remove_ip, +}; +use crate::service::{ ServiceArgs, ServiceCommands, describe_service, list_services }; +use crate::status::{ StatusArgs, status_command }; use crate::uninstall::uninstall; -use crate::essential::GeneralData; use crate::essential::update_config_metadata; #[derive(Parser, Debug)] #[command( - author = GeneralData::AUTHOR, - version = GeneralData::VERSION, - about = None, - long_about = None + author = env!("CARGO_PKG_AUTHORS"), + version = env!("CARGO_PKG_VERSION"), + about = env!("CARGO_PKG_DESCRIPTION"), + long_about = env!("CARGO_PKG_DESCRIPTION") )] struct Cli { //name: String, @@ -44,9 +46,6 @@ struct Cli { #[derive(Subcommand, Debug, Clone)] enum Commands { /* list of available commands */ - #[command(name = "set-env")] SetEnv(SetArgs), - #[command(name = "get-env")] - GetEnv, #[command(name = "install", about = "Manage installation")] Install(InstallArgs), #[command(name = "uninstall", about = "Manage uninstallation")] Uninstall, @@ -65,130 +64,105 @@ struct SetArgs { val: String, } -async fn args_parser() -> Result<(), Error> { +async fn args_parser() -> Result<(), CliError> { let args = Cli::parse(); - let env = "kubernetes".to_string(); - let general_data = GeneralData::new(env); debug!("Arguments {:?}", args.cmd); match args.cmd { - Some(Commands::SetEnv(env)) => { - general_data.set_env(env.val); - Ok(()) - } - Some(Commands::GetEnv) => { - general_data.get_env_output(); - Ok(()) - } - Some(Commands::Install(installation_args)) => match installation_args.install_cmd { - InstallCommands::All => { - install_cortexflow().await; - Ok(()) - } - InstallCommands::TestPods => { - install_simple_example(); - Ok(()) + Some(Commands::Install(installation_args)) => + match installation_args.install_cmd { + InstallCommands::All => { + install_cortexflow().await.map_err(|e| eprintln!("{}",e) )?; + } + InstallCommands::TestPods => { + install_simple_example().await.map_err(|e| eprintln!("{}",e) )?; + } } - }, Some(Commands::Uninstall) => { - uninstall(); - Ok(()) + uninstall().await.map_err(|e| eprintln!("{}",e) )?; } Some(Commands::Update) => { update_cli(); - Ok(()) } Some(Commands::Info) => { - info(general_data); - Ok(()) + info(); } - Some(Commands::Service(service_args)) => match service_args.service_cmd { - ServiceCommands::List { namespace } => { - Some(list_services(namespace)); - Ok(()) - } - ServiceCommands::Describe { - service_name, - namespace, - } => { - describe_service(service_name, &namespace); - Ok(()) + Some(Commands::Service(service_args)) => + match service_args.service_cmd { + ServiceCommands::List { namespace } => { + list_services(namespace).await.map_err(|e| eprintln!("{}",e) )?; + } + ServiceCommands::Describe { service_name, namespace } => { + describe_service(service_name, &namespace).await.map_err(|e| eprintln!("{}",e) )?; + } } - }, Some(Commands::Status(status_args)) => { - status_command(status_args.output, status_args.namespace); - Ok(()) + status_command(status_args.output, status_args.namespace).await.map_err(|e| eprintln!("{}",e) )?; } Some(Commands::Logs(logs_args)) => { - logs_command(logs_args.service, logs_args.component, logs_args.namespace); - Ok(()) + logs_command(logs_args.service, logs_args.component, logs_args.namespace).await.map_err(|e| eprintln!("{}",e) )?; } - Some(Commands::Monitor(monitor_args)) => match monitor_args.monitor_cmd { - MonitorCommands::List => { - let _ = list_features().await; - Ok(()) - } - MonitorCommands::Connections => { - let _ = monitor_identity_events().await; - Ok(()) + Some(Commands::Monitor(monitor_args)) => + match monitor_args.monitor_cmd { + MonitorCommands::List => { + let _ = list_features().await.map_err(|e| eprintln!("{}",e) )?; + } + MonitorCommands::Connections => { + let _ = monitor_identity_events().await.map_err(|e| eprintln!("{}",e) )?; + } } - }, Some(Commands::Policies(policies_args)) => { match policies_args.policy_cmd { PoliciesCommands::CheckBlocklist => { - let _ = check_blocklist().await; - Ok(()) + let _ = check_blocklist().await.map_err(|e| eprintln!("{}",e) )?; } PoliciesCommands::CreateBlocklist => { // pass the ip as a monitoring flag match policies_args.flags { None => { - println!("{}", "Insert at least one ip to create a blocklist".red()); - Ok(()) + eprintln!("{}", "Insert at least one ip to create a blocklist".red()); } - Some(exclude_flag) => { - println!("inserted ip: {} ", exclude_flag); + Some(ip) => { + println!("inserted ip: {} ", ip); //insert the ip in the blocklist - match create_blocklist(&exclude_flag).await { + match create_blocklist(&ip).await { Ok(_) => { //update the config metadata - let _ = update_config_metadata(&exclude_flag, "add").await; + let _ = update_config_metadata(&ip, "add").await.map_err(|e| eprintln!("{}",e) )?; } Err(e) => { - println!("{}", e); + eprintln!("{}", e); } } - Ok(()) } } } - PoliciesCommands::RemoveIpFromBlocklist => match policies_args.flags { - None => { - println!( - "{}", - "Insert at least one ip to remove from the blocklist".red() - ); - Ok(()) - } - Some(ip) => { - println!("Inserted ip: {}", ip); - match remove_ip(&ip).await { - Ok(_) => { - let _ = update_config_metadata(&ip, "delete").await; - } - Err(e) => { - println!("{}", e); + PoliciesCommands::RemoveIpFromBlocklist => + match policies_args.flags { + None => { + eprintln!( + "{}", + "Insert at least one ip to remove from the blocklist".red() + ); + } + Some(ip) => { + println!("Inserted ip: {}", ip); + match remove_ip(&ip).await { + Ok(_) => { + let _ = update_config_metadata(&ip, "delete").await.map_err(|e| eprintln!("{}",e) )?; + } + Err(e) => { + eprintln!("{}", e); + } } } - Ok(()) } - }, } } None => { eprintln!("CLI unknown argument. Cli arguments passed: {:?}", args.cmd); - Ok(()) } } + Ok(()) } #[tokio::main] diff --git a/cli/src/service.rs b/cli/src/service.rs index 42788a1..b66ed7e 100644 --- a/cli/src/service.rs +++ b/cli/src/service.rs @@ -1,11 +1,10 @@ -use std::process::exit; -use std::str; -use std::{ io::Error, process::Command }; - -use crate::essential::{ Environments, get_config_directory, read_configs }; +use std::{ str, process::Command }; use colored::Colorize; - use clap::{ Args, Subcommand }; +use kube::{ core::ErrorResponse, Error }; + +use crate::essential::{ BASE_COMMAND, connect_to_client, CliError }; +use crate::logs::{ get_available_namespaces, check_namespace_exists }; //service subcommands #[derive(Subcommand, Debug, Clone)] @@ -26,77 +25,30 @@ pub struct ServiceArgs { pub service_cmd: ServiceCommands, } -fn check_namespace_exists(namespace: &str) -> bool { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env).args(["get", "namespace", namespace]).output(); - - match output { - Ok(output) => output.status.success(), - Err(_) => false, - } - } - Err(_) => false, - } -} - -fn get_available_namespaces() -> Vec { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) - .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(), - } - } - Err(_) => Vec::new(), - } -} - -pub fn list_services(namespace: Option) -> Result<(), Error> { - //TODO: maybe we can list both services and pods - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); +// docs: +// +// This is the main function that lists all the services in the cluster +// Steps: +// - connects to kubernetes client +// - check if the namespace exists +// - if the cortexflow namespace exists returns the service list +// - else return an empty Vector +// +// +// Returns a CliError if the connection fails + +pub async fn list_services(namespace: Option) -> Result<(), CliError> { + //TODO: maybe we can list both services and pods? + + match connect_to_client().await { + Ok(_) => { let ns = namespace.unwrap_or_else(|| "cortexflow".to_string()); println!("{} {} {}", "=====>".blue().bold(), "Listing services in namespace:", ns); // Check if namespace exists first - if !check_namespace_exists(&ns) { - let available_namespaces = get_available_namespaces(); + if !check_namespace_exists(&ns).await? { + let available_namespaces = get_available_namespaces().await?; println!("\nāŒ Namespace '{}' not found", ns); println!("{}", "=".repeat(50)); @@ -109,19 +61,18 @@ pub fn list_services(namespace: Option) -> Result<(), Error> { } else { println!("No namespaces found in the cluster."); } - - std::process::exit(1); } // kubectl command to get services - let output = Command::new(env).args(["get", "svc", "-n", &ns, "--no-headers"]).output(); + let output = Command::new(BASE_COMMAND) + .args(["get", "svc", "-n", &ns, "--no-headers"]) + .output(); match output { Ok(output) => { if !output.status.success() { let error = str::from_utf8(&output.stderr).unwrap_or("Unknown error"); - eprintln!("Error executing {}: {}", env, error); - std::process::exit(1); + eprintln!("Error executing {}: {}", BASE_COMMAND, error); } let stdout = str::from_utf8(&output.stdout).unwrap_or(""); @@ -133,7 +84,6 @@ pub fn list_services(namespace: Option) -> Result<(), Error> { "No services found in namespace", ns ); - exit(1); } // header for Table @@ -165,94 +115,147 @@ pub fn list_services(namespace: Option) -> Result<(), Error> { ); } } + Ok(()) } Err(err) => { - eprintln!("Failed to execute {} command: {}", env, err); - eprintln!("Make sure {} is installed and configured properly", env); - std::process::exit(1); + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to execute the kubectl command".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } } } - Err(e) => { - eprintln!("Error reading the cluster environment from config files: {:?}", e); + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } } - Ok(()) } -pub fn describe_service(service_name: String, namespace: &Option) { - match list_services(namespace.clone()) { +// docs: +// +// This is the main function to describe a kubernetes service +// Steps: +// - connects to kubernetes client +// - check if the namespace exists +// - if the cortexflow namespace exists executes the kubectl describe command +// - output the result of the command +// - else return an empty Vector +// +// +// Returns a CliError if the connection fails + +pub async fn describe_service( + service_name: String, + namespace: &Option +) -> Result<(), CliError> { + match connect_to_client().await { Ok(_) => { - let file_path = get_config_directory().unwrap().1; - - let env = "kubectl".to_string(); - - let ns = namespace.clone().unwrap_or_else(|| "cortexflow".to_string()); - - println!( - "{} {} {} {} {}", - "=====>".blue().bold(), - "Describing service", - "in namespace:", - service_name, - ns - ); - println!("{}", "=".repeat(60)); - - // Check if namespace exists first - 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); - } - println!("\nTry: cortex service describe {} --namespace ", service_name); - } else { - println!("No namespaces found in the cluster."); - } - - std::process::exit(1); - } - - // Execute kubectl describe pod command - let output = Command::new(env) - .args(["describe", "pod", &service_name, "-n", &ns]) - .output(); - - match output { - Ok(output) => { - if !output.status.success() { - let error = str::from_utf8(&output.stderr).unwrap_or("Unknown error"); - eprintln!("Error executing kubectl describe: {}", error); - eprintln!( - "Make sure the pod '{}' exists in namespace '{}'", - service_name, - ns - ); - std::process::exit(1); + match list_services(namespace.clone()).await { + Ok(_) => { + //let file_path = get_config_directory().unwrap().1; + + let ns = namespace.clone().unwrap_or_else(|| "cortexflow".to_string()); + + println!( + "{} {} {} {} {}", + "=====>".blue().bold(), + "Describing service", + "in namespace:", + service_name, + ns + ); + println!("{}", "=".repeat(60)); + + // Check if namespace exists first + if !check_namespace_exists(&ns).await? { + let available_namespaces = get_available_namespaces().await?; + + 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); + } + println!("\nTry: cortex service describe {} --namespace ", service_name); + } else { + println!("No namespaces found in the cluster."); + } } - let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - - if stdout.trim().is_empty() { - println!("No description found for pod '{}'", service_name); + // Execute kubectl describe pod command + let output = Command::new(BASE_COMMAND) + .args(["describe", "pod", &service_name, "-n", &ns]) + .output(); + + match output { + Ok(output) => { + if !output.status.success() { + let error = str + ::from_utf8(&output.stderr) + .unwrap_or("Unknown error"); + eprintln!("Error executing kubectl describe: {}", error); + eprintln!( + "Make sure the pod '{}' exists in namespace '{}'", + service_name, + ns + ); + } + + let stdout = str::from_utf8(&output.stdout).unwrap_or(""); + + if stdout.trim().is_empty() { + println!("No description found for pod '{}'", service_name); + } + + // Print the full kubectl describe output + println!("{}", stdout); + Ok(()) + } + Err(err) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to execute the kubectl command ".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } - - // Print the full kubectl describe output - println!("{}", stdout); - } - Err(err) => { - eprintln!("Failed to execute kubectl describe command: {}", err); - eprintln!("Make sure kubectl is installed and configured properly"); - std::process::exit(1); } + Err(e) => todo!(), } } - Err(_) => todo!(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } diff --git a/cli/src/status.rs b/cli/src/status.rs index 772a64a..2680781 100644 --- a/cli/src/status.rs +++ b/cli/src/status.rs @@ -1,11 +1,10 @@ use colored::Colorize; -use std::process::Command; -use std::str; - -use crate::essential::{Environments, get_config_directory, read_configs}; - +use std::{ process::Command, str }; use clap::Args; +use kube::{ Error, core::ErrorResponse }; +use crate::logs::{ get_available_namespaces, check_namespace_exists }; +use crate::essential::{ BASE_COMMAND, connect_to_client, CliError }; #[derive(Debug)] pub enum OutputFormat { @@ -32,230 +31,237 @@ pub struct StatusArgs { pub namespace: Option, } -pub fn status_command(output_format: Option, namespace: Option) { - let format = output_format - .map(OutputFormat::from) - .unwrap_or(OutputFormat::Text); - let ns = namespace.unwrap_or_else(|| "cortexflow".to_string()); - - println!( - "{} {} {}", - "=====>".blue().bold(), - "Checking CortexFlow status for namespace: ", - ns - ); - - // namespace checking - let namespace_status = check_namespace_exists(&ns); - - // If namespace doesn't exist, display error with available namespaces and exit - if !namespace_status { - let available_namespaces = get_available_namespaces(); - - match format { - OutputFormat::Text => { - println!("\nāŒ Namespace Status Check Failed"); - println!("{}", "=".repeat(50)); - println!(" āŒ {} namespace: NOT FOUND", ns); - - if !available_namespaces.is_empty() { - println!("\nšŸ“‹ Available namespaces:"); - for available_ns in &available_namespaces { - println!(" • {}", available_ns); +// docs: +// +// This is the main function for the status command. The status command display all the pods and services status in 3 types of format : Text, Json,Yaml +// defaul type is Text +// +// Steps: +// - connects to kubernetes client +// - check if the given namespace exists +// - if the namespace exists +// - return the pods status and the service status for all the pods and services in the namespace +// - else +// - return a failed state +// +// Returns a CliError if the connection fails + +pub async fn status_command( + output_format: Option, + namespace: Option +) -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { + let format = output_format.map(OutputFormat::from).unwrap_or(OutputFormat::Text); + let ns = namespace.unwrap_or_else(|| "cortexflow".to_string()); + + println!( + "{} {} {}", + "=====>".blue().bold(), + "Checking CortexFlow status for namespace: ", + ns + ); + + // namespace checking + let namespace_status = check_namespace_exists(&ns).await?; + + // If namespace doesn't exist, display error with available namespaces and exit + if !namespace_status { + let available_namespaces = get_available_namespaces().await?; + + match format { + OutputFormat::Text => { + println!("\nāŒ Namespace Status Check Failed"); + println!("{}", "=".repeat(50)); + println!(" āŒ {} namespace: NOT FOUND", ns); + + if !available_namespaces.is_empty() { + println!("\nšŸ“‹ Available namespaces:"); + for available_ns in &available_namespaces { + println!(" • {}", available_ns); + } + } + } + OutputFormat::Json => { + println!("{{"); + println!(" \"error\": \"{} namespace not found\",", ns); + println!(" \"namespace\": {{"); + println!(" \"name\": \"{}\",", ns); + println!(" \"exists\": false"); + println!(" }},"); + println!(" \"available_namespaces\": ["); + for (i, ns) in available_namespaces.iter().enumerate() { + let comma = if i == available_namespaces.len() - 1 { "" } else { "," }; + println!(" \"{}\"{}", ns, comma); + } + println!(" ]"); + println!("}}"); + } + OutputFormat::Yaml => { + println!("error: {} namespace not found", ns); + println!("namespace:"); + println!(" name: {}", ns); + println!(" exists: false"); + println!("available_namespaces:"); + for ns in &available_namespaces { + println!(" - {}", ns); + } } } } - OutputFormat::Json => { - println!("{{"); - println!(" \"error\": \"{} namespace not found\",", ns); - println!(" \"namespace\": {{"); - println!(" \"name\": \"{}\",", ns); - println!(" \"exists\": false"); - println!(" }},"); - println!(" \"available_namespaces\": ["); - for (i, ns) in available_namespaces.iter().enumerate() { - let comma = if i == available_namespaces.len() - 1 { - "" - } else { - "," - }; - println!(" \"{}\"{}", ns, comma); - } - println!(" ]"); - println!("}}"); - } - OutputFormat::Yaml => { - println!("error: {} namespace not found", ns); - println!("namespace:"); - println!(" name: {}", ns); - println!(" exists: false"); - println!("available_namespaces:"); - for ns in &available_namespaces { - println!(" - {}", ns); - } - } - } - std::process::exit(1); - } - - // get pods and services only if namespace exists - let pods_status = get_pods_status(&ns); - let services_status = get_services_status(&ns); - - // display options (format) - match format { - OutputFormat::Text => { - display_text_format(&ns, namespace_status, pods_status, services_status) - } - OutputFormat::Json => { - display_json_format(&ns, namespace_status, pods_status, services_status) - } - OutputFormat::Yaml => { - display_yaml_format(&ns, namespace_status, pods_status, services_status) - } - } -} - -fn check_namespace_exists(namespace: &str) -> bool { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) - .args(["get", "namespace", namespace]) - .output(); - - match output { - Ok(output) => output.status.success(), - Err(_) => false, - } - } - Err(_) => false, - } -} -fn get_available_namespaces() -> Vec { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) - .args([ - "get", - "namespaces", - "--no-headers", - "-o", - "custom-columns=NAME:.metadata.name", - ]) - .output(); + // get pods and services only if namespace exists + let pods_status = get_pods_status(&ns).await?; + let services_status = get_services_status(&ns).await?; - 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() + // display options (format) + match format { + OutputFormat::Text => { + display_text_format(&ns, namespace_status, pods_status, services_status); + Ok(()) + } + OutputFormat::Json => { + display_json_format(&ns, namespace_status, pods_status, services_status); + Ok(()) + } + OutputFormat::Yaml => { + display_yaml_format(&ns, namespace_status, pods_status, services_status); + Ok(()) } - _ => Vec::new(), } } - Err(_) => Vec::new(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } -fn get_pods_status(namespace: &str) -> Vec<(String, String, String)> { - let file_path = get_config_directory().unwrap().1; - - let env_from_file = "kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) +// docs: +// +// This is an auxiliary function that returns the status for a given pod +// Steps: +// - connects to kubernetes client +// - return the pod status in this format: (name,ready?,status) +// +// Returns a CliError if the connection fails + +async fn get_pods_status(namespace: &str) -> Result, CliError> { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND) .args(["get", "pods", "-n", namespace, "--no-headers"]) .output(); match output { Ok(output) if output.status.success() => { let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - stdout - .lines() - .filter_map(|line| { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 3 { - Some(( - parts[0].to_string(), // name - parts[1].to_string(), // ready - parts[2].to_string(), // status - )) - } else { - None - } - }) - .collect() + Ok( + stdout + .lines() + .filter_map(|line| { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 3 { + Some(( + parts[0].to_string(), // name + parts[1].to_string(), // ready + parts[2].to_string(), // status + )) + } else { + None + } + }) + .collect() + ) } - _ => Vec::new(), + _ => Ok(Vec::new()), } } - Err(_) => Vec::new(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } -fn get_services_status(namespace: &str) -> Vec<(String, String, String)> { - let file_path = get_config_directory().unwrap().1; - - let env_from_file ="kubernetes".to_string(); - let user_env = Environments::try_from(env_from_file.to_lowercase()); - - match user_env { - Ok(cluster_environment) => { - let env = cluster_environment.base_command(); - let output = Command::new(env) +// docs: +// +// This is an auxiliary function that returns the status for a given service +// Steps: +// - connects to kubernetes client +// - return the service status in this format: (name,type,cluster ips) +// +// Returns a CliError if the connection fails + +async fn get_services_status(namespace: &str) -> Result, CliError> { + match connect_to_client().await { + Ok(_) => { + let output = Command::new(BASE_COMMAND) .args(["get", "services", "-n", namespace, "--no-headers"]) .output(); match output { Ok(output) if output.status.success() => { let stdout = str::from_utf8(&output.stdout).unwrap_or(""); - stdout - .lines() - .filter_map(|line| { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() >= 4 { - Some(( - parts[0].to_string(), // name - parts[1].to_string(), // type - parts[2].to_string(), // cluster ips - )) - } else { - None - } - }) - .collect() + Ok( + stdout + .lines() + .filter_map(|line| { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 4 { + Some(( + parts[0].to_string(), // name + parts[1].to_string(), // type + parts[2].to_string(), // cluster ips + )) + } else { + None + } + }) + .collect() + ) } - _ => Vec::new(), + _ => Ok(Vec::new()), } } - Err(_) => Vec::new(), + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } +// docs: displays outputs in a text format + fn display_text_format( ns: &str, namespace_exists: bool, pods: Vec<(String, String, String)>, - services: Vec<(String, String, String)>, + services: Vec<(String, String, String)> ) { println!("\nšŸ” CortexFlow Status Report"); println!("{}", "=".repeat(50)); @@ -289,11 +295,13 @@ fn display_text_format( println!("\n{}", "=".repeat(50)); } +// docs: displays outputs in a json format + fn display_json_format( ns: &str, namespace_exists: bool, pods: Vec<(String, String, String)>, - services: Vec<(String, String, String)>, + services: Vec<(String, String, String)> ) { println!("{{"); println!(" \"namespace\": {{"); @@ -325,11 +333,13 @@ fn display_json_format( println!("}}"); } +// docs: displays outputs in a yaml format + fn display_yaml_format( ns: &str, namespace_exists: bool, pods: Vec<(String, String, String)>, - services: Vec<(String, String, String)>, + services: Vec<(String, String, String)> ) { println!("namespace:"); println!(" name: {}", ns); diff --git a/cli/src/uninstall.rs b/cli/src/uninstall.rs index 91e6329..c28cff5 100644 --- a/cli/src/uninstall.rs +++ b/cli/src/uninstall.rs @@ -1,127 +1,165 @@ -use crate::essential::{Environments, get_config_directory, read_configs}; use colored::Colorize; -use std::io::stdin; -use std::process::Command; -use tracing::debug; - -use std::thread; -use std::time::Duration; - -pub fn uninstall() { - //let file_path = get_config_directory().unwrap().1; - //let dir_config_path = get_config_directory().unwrap().0; - //debug!("file_path variable:{:?}", dir_config_path); - //let env_from_file = read_configs(file_path.clone()); - //let user_env = Environments::try_from(env_from_file.to_lowercase()); - - //match user_env { - // Ok(cluster_environment) => { - let env = "kubectl".to_string(); - println!( - "{} {}", - "=====>".blue().bold(), - "Uninstalling cortexflow..." - ); - let mut userinput: String = String::new(); - println!("{} {}", "=====>".blue().bold(), "Select one option:"); - display_uninstall_options(); - stdin() - .read_line(&mut userinput) - .expect("Error reading user input"); - - let trimmed_input = userinput.trim(); - if trimmed_input == "1" { - uninstall_all(&env); - println!( - "{} {}", - "=====>".blue().bold(), - "Do you want to remove the command line metadata? [y/n]" - ); +use std::{ io::stdin, process::Command, time::Duration, thread }; + +use crate::essential::{ BASE_COMMAND, CliError, connect_to_client }; +use kube::{ Error, core::ErrorResponse }; + +//docs: +// +// This function manages the uninstall process for the cortexflow components +// Steps: +// - connects to kubernetes client +// - display the uninstall options +// - read the user input (e.g. 1 > all components) +// - uninstall the selected component or the whole namespace +// +// Returns an CliError if something fails + +pub async fn uninstall() -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { + println!("{} {}", "=====>".blue().bold(), "Uninstalling cortexflow..."); + let mut userinput: String = String::new(); + println!("{} {}", "=====>".blue().bold(), "Select one option:"); + display_uninstall_options(); + stdin().read_line(&mut userinput).expect("Error reading user input"); - //clear the user input before assigning a new value - userinput.clear(); - stdin() - .read_line(&mut userinput) - .expect("Error reading user input"); - - if userinput.trim() == "y" { - println!( - "{} {}", - "=====>".blue().bold(), - "Deleting metadata config files" - ); - //println!( - // "{} {}: {:?}", - // "=====>".blue().bold(), - // "Removing", - // dir_config_path.clone() - //); - //rm_dir(dir_config_path.as_os_str().to_str().unwrap()); - } else if userinput.trim() == "n" { - println!( - "{} {}", - "=====>".blue().bold(), - "Skipping metadata config files deletion" - ); + let trimmed_input = userinput.trim(); + if trimmed_input == "1" { + uninstall_all().await?; + } else if trimmed_input == "2" { + uninstall_component("deployment", "cortexflow-identity").await?; + } + Ok(()) + } + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) } - } else if trimmed_input == "2" { - uninstall_component("deployment", "cortexflow-identity", &env.to_owned()); } } -// Err(e) => println!("An error occured while reading the config files: {}", e), -//} -//} + +//docs: +// +// This function only print the uninstall options fn display_uninstall_options() { println!("{} {}", "=====>".blue().bold(), "1 > all"); println!("{} {}", "=====>".blue().bold(), "2 > identity-service"); } -fn uninstall_all(env: &str) { - println!( - "{} {}", - "=====>".blue().bold(), - "Deleting cortexflow components".red() - ); - //uninstall_component("namespace", "cortexflow", env); - let output = Command::new(env) - .args(["delete", "namespace", "cortexflow"]) - .output() - .expect("Error deleting cortexflow namespace"); +//docs: +// +// This function manages the uninstall of the whole cortexflow namespace +// Steps: +// - connects to kubernetes client +// - execute the command to uninstall the cortexflow namespace +// +// Returns an CliError if something fails - if !output.status.success() { - eprintln!( - "Error deleting cortexflow namespace:\n{}", - String::from_utf8_lossy(&output.stderr) - ); - } else { - println!("āœ… Removed cortexflow namespace"); +async fn uninstall_all() -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { + println!("{} {}", "=====>".blue().bold(), "Deleting cortexflow components".red()); + let output = Command::new(BASE_COMMAND) + .args(["delete", "namespace", "cortexflow"]) + .output() + .map_err(|e| CliError::InstallerError { + reason: format!("Failed to execute delete command: {}", e), + })?; + + if output.status.success() { + println!("āœ… Removed cortexflow namespace"); + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("Error deleting cortexflow namespace. Error: {} ", stderr); + Err(CliError::InstallerError { + reason: format!("Failed to delete cortexflow namespace. Error: {}", stderr), + }) + } + } + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } -fn uninstall_component(component_type: &str, component: &str, env: &str) { - println!( - "{} {} {}", - "=====>".blue().bold(), - "Deleting service", - component - ); - let output = Command::new(env) - .args(["delete", component_type, component, "-n", "cortexflow"]) - .output() - .expect("Error deleting cortexflow-identity"); +//docs: +// +// This function manages the uninstall of given cortexflow components +// Steps: +// - connects to kubernetes client +// - executes the command to uninstall a given component +// +// Returns an InstallerError if something fails - if !output.status.success() { - eprintln!( - "Error deleting: {}:\n{}", - component, - String::from_utf8_lossy(&output.stderr) - ); - } else { - println!("āœ… Removed component {}", component); +async fn uninstall_component( + component_type: &str, + component: &str +) -> Result<(), CliError> { + match connect_to_client().await { + Ok(_) => { + println!("{} {} {}", "=====>".blue().bold(), "Deleting service", component); + + let output = Command::new(BASE_COMMAND) + .args(["delete", component_type, component, "-n", "cortexflow"]) + .output() + .map_err(|e| CliError::InstallerError { + reason: format!("Failed to execute delete command: {}", e), + })?; + + if output.status.success() { + println!("āœ… Removed component {}", component); + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("Error deleting {}:\n{}", component, stderr); + Err(CliError::InstallerError { + reason: format!("Failed to delete component '{}': {}", component, stderr), + }) + } + } + Err(_) => { + Err( + CliError::ClientError( + Error::Api(ErrorResponse { + status: "failed".to_string(), + message: "Failed to connect to kubernetes client".to_string(), + reason: "Your cluster is probably disconnected".to_string(), + code: 404, + }) + ) + ) + } } } +// +// +//docs: +// +// This function is deprecated and will be removed in the next version +// +// Do not include or refactor this function +#[deprecated(since = "0.1.4")] fn rm_dir(directory_to_remove: &str) { let output = Command::new("rm") .args(["-rf", directory_to_remove]) diff --git a/core/Cargo.lock b/core/Cargo.lock index 38ac516..a77df17 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -448,7 +448,6 @@ dependencies = [ "kube", "libc", "nix", - "serde_yaml", "tokio", "tracing", "tracing-subscriber", diff --git a/core/src/components/identity/Cargo.toml b/core/src/components/identity/Cargo.toml index e53dcdc..125d2ec 100644 --- a/core/src/components/identity/Cargo.toml +++ b/core/src/components/identity/Cargo.toml @@ -31,4 +31,3 @@ cortexbrain-common = { path = "../../../common" } nix = { version = "0.30.1", features = ["net"] } kube = {version = "2.0.1",features = ["client"]} k8s-openapi = {version ="0.26.0", features = ["v1_34"]} -serde_yaml = "0.9.34"