diff --git a/src/cli/Cargo.toml b/src/cli/Cargo.toml index 957c55c..2b22a86 100644 --- a/src/cli/Cargo.toml +++ b/src/cli/Cargo.toml @@ -3,15 +3,14 @@ name = "cli" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -clap = { version = "4.5.3", features = ["derive"] } -toml = "0.8.12" -tokio = { version = "1.36.0", features = ["full"] } -serde = { version = "1.0.197", features = ["derive"] } -serde_yaml = "0.9.34" -schemars = "0.8.16" -serde_json = "1.0.115" -reqwest = "0.12.3" -shared_models = { path="../shared-models" } +clap = { version = "4.5", features = ["derive"] } +crossterm = "0.27" +futures = "0.3" +reqwest = { version = "0.12", features = ["json"] } +reqwest-eventsource = "0.6" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +shared_models = { path = "../shared-models" } +tokio = { version = "1.38", features = ["full"] } +toml = "0.8" diff --git a/src/cli/config/config.template.yaml b/src/cli/config/config.template.yaml deleted file mode 100644 index 73dfaad..0000000 --- a/src/cli/config/config.template.yaml +++ /dev/null @@ -1,4 +0,0 @@ -language: rust -env_path: /path/cloudlet/src/cli/config/example.env -code_path: /path/cloudlet/src/cli/src/main.rs -log_level: debug diff --git a/src/cli/config/config.yaml b/src/cli/config/config.yaml deleted file mode 100644 index d5a19be..0000000 --- a/src/cli/config/config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -language: rust -env_path: /home/thomas/Desktop/fork/cloudlet/src/cli/config/example.env -code_path: /home/thomas/Desktop/fork/cloudlet/src/cli/src/main.rs -log_level: debug diff --git a/src/cli/config/example.env b/src/cli/config/example.env deleted file mode 100644 index 8c01b5b..0000000 --- a/src/cli/config/example.env +++ /dev/null @@ -1,3 +0,0 @@ -HOST="localhost" -PORT=3000 -PASSWORD=3456 \ No newline at end of file diff --git a/src/cli/src/api_client/execute.rs b/src/cli/src/api_client/execute.rs new file mode 100644 index 0000000..d5ffd91 --- /dev/null +++ b/src/cli/src/api_client/execute.rs @@ -0,0 +1,38 @@ +use super::Error; +use reqwest_eventsource::EventSource; +use serde::Deserialize; +use shared_models::CloudletDtoRequest; + +pub async fn execute(base_url: &str, request: CloudletDtoRequest) -> Result { + let client = reqwest::Client::new() + .post(format!("{base_url}/run")) + .json(&request); + + EventSource::new(client).map_err(Error::CreateEventSource) +} + +#[derive(Debug, Deserialize)] +pub struct ExecuteJsonResponse { + pub stage: Stage, + pub stdout: Option, + pub stderr: Option, + pub exit_code: Option, +} + +#[derive(Debug, Deserialize)] +pub enum Stage { + Pending, + Building, + Running, + Done, + Failed, + Debug, +} + +impl TryFrom for ExecuteJsonResponse { + type Error = Error; + + fn try_from(value: String) -> Result { + serde_json::from_str(&value).map_err(|_| Error::ExecuteResponseDeserialize) + } +} diff --git a/src/cli/src/api_client/mod.rs b/src/cli/src/api_client/mod.rs new file mode 100644 index 0000000..bcfcd89 --- /dev/null +++ b/src/cli/src/api_client/mod.rs @@ -0,0 +1,50 @@ +use crate::utils; +use serde::Deserialize; +use shared_models::{BuildConfig, CloudletDtoRequest, Language, ServerConfig}; +use std::{fs, path::PathBuf}; + +pub mod execute; +pub mod shutdown; + +pub use execute::*; +pub use shutdown::*; + +#[derive(Debug)] +pub enum Error { + ReadTomlConfigFile(std::io::Error), + TomlConfigParse(toml::de::Error), + ReadCodeFile(std::io::Error), + ExecuteRequestBody, + CreateEventSource(reqwest_eventsource::CannotCloneRequestError), + ExecuteResponseDeserialize, + ShutdownSendRequest(reqwest::Error), + ShutdownResponse(reqwest::Error), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct TomlConfig { + workload_name: String, + language: Language, + action: String, + server: ServerConfig, + build: BuildConfig, +} + +pub fn new_cloudlet_request(config_path: &PathBuf) -> Result { + let toml_file = fs::read_to_string(config_path).map_err(Error::ReadTomlConfigFile)?; + let config: TomlConfig = toml::from_str(&toml_file).map_err(Error::TomlConfigParse)?; + + let source_code_path = &config.build.source_code_path; + let code: String = utils::read_file(source_code_path).map_err(Error::ReadCodeFile)?; + + Ok(CloudletDtoRequest { + workload_name: config.workload_name, + language: config.language, + code, + log_level: shared_models::LogLevel::INFO, + server: config.server, + build: config.build, + action: config.action, + }) +} diff --git a/src/cli/src/api_client/shutdown.rs b/src/cli/src/api_client/shutdown.rs new file mode 100644 index 0000000..87b6e1c --- /dev/null +++ b/src/cli/src/api_client/shutdown.rs @@ -0,0 +1,15 @@ +use super::Error; +use shared_models::CloudletShutdownResponse; + +pub async fn shutdown(base_url: &str) -> Result { + let client = reqwest::Client::new(); + + client + .post(format!("{base_url}/shutdown")) + .send() + .await + .map_err(Error::ShutdownSendRequest)? + .json::() + .await + .map_err(Error::ShutdownSendRequest) +} diff --git a/src/cli/src/args.rs b/src/cli/src/args.rs index 3c5b252..7545818 100644 --- a/src/cli/src/args.rs +++ b/src/cli/src/args.rs @@ -14,5 +14,5 @@ pub enum Commands { #[arg(short, long)] config_path: PathBuf, }, - Shutdown {}, + Shutdown, } diff --git a/src/cli/src/lib.rs b/src/cli/src/lib.rs new file mode 100644 index 0000000..599177e --- /dev/null +++ b/src/cli/src/lib.rs @@ -0,0 +1,69 @@ +use api_client::ExecuteJsonResponse; +use args::{CliArgs, Commands}; +use crossterm::style::Stylize; +use futures::TryStreamExt; +use reqwest_eventsource::Event; +use std::fmt::Display; + +mod api_client; +pub mod args; +mod utils; + +#[derive(Debug)] +pub enum Error { + StdoutExecute(std::io::Error), + ApiClient(api_client::Error), + InvalidRequest(String), + ProgramFailed, +} + +pub async fn run_cli(base_url: &str, args: CliArgs) -> Result { + match args.command { + Commands::Run { config_path } => { + let body = api_client::new_cloudlet_request(&config_path).map_err(Error::ApiClient)?; + let mut es = api_client::execute(base_url, body) + .await + .map_err(Error::ApiClient)?; + + let mut exit_code = 0; + + while let Ok(Some(event)) = es.try_next().await { + match event { + Event::Open => { /* skip */ } + Event::Message(msg) => { + let exec_response = ExecuteJsonResponse::try_from(msg.data); + if let Ok(exec_response) = exec_response { + if let Some(stdout) = exec_response.stdout { + println!("{}", stylize(stdout, &exec_response.stage)); + } + if let Some(stderr) = exec_response.stderr { + println!("{}", stylize(stderr, &exec_response.stage)); + } + if let Some(code) = exec_response.exit_code { + exit_code = code; + } + } + } + } + } + + Ok(exit_code) + } + Commands::Shutdown {} => { + api_client::shutdown(base_url) + .await + .map_err(Error::ApiClient)?; + + Ok(0) + } + } +} + +fn stylize(output: String, stage: &api_client::Stage) -> impl Display { + match stage { + api_client::Stage::Building => output.yellow(), + api_client::Stage::Failed => output.dark_red(), + api_client::Stage::Debug => output.dark_blue(), + _ => output.stylize(), + } +} diff --git a/src/cli/src/main.rs b/src/cli/src/main.rs index 35f10cc..9e8254d 100644 --- a/src/cli/src/main.rs +++ b/src/cli/src/main.rs @@ -1,49 +1,20 @@ use clap::Parser; - -use args::{CliArgs, Commands}; - -use services::CloudletClient; -use std::{fs, io, process::exit}; - -mod args; -mod services; -mod utils; +use cli::args::CliArgs; +use std::process::exit; #[tokio::main] -async fn main() -> io::Result<()> { +async fn main() { let args = CliArgs::parse(); - match args.command { - Commands::Run { config_path } => { - let toml_file = match fs::read_to_string(config_path.clone()) { - Ok(c) => c, - Err(_) => { - eprintln!("Could not read file `{:?}`", config_path); - exit(1); - } - }; - let body = CloudletClient::new_cloudlet_config(toml_file); - let response = CloudletClient::run(body).await; + let api_url = std::env::var("API_URL").unwrap_or("localhost:3000".into()); + let api_url = format!("http://{api_url}"); - match response { - Ok(_) => println!("Request successful {:?}", response), - Err(e) => eprintln!("Error while making the request: {}", e), - } - } - Commands::Shutdown {} => { - let response = CloudletClient::shutdown().await; - match response { - Ok(bool) => { - if bool { - println!("Shutdown Request successful !") - } else { - println!("Shutdown Request Failed") - } - } - Err(()) => println!("Cannot send shutdown Request"), - } + let result = cli::run_cli(&api_url, args).await; + match result { + Ok(exit_code) => exit(exit_code), + Err(e) => { + eprintln!("Could not execute the command:\n{:?}", e); + exit(1); } } - - Ok(()) } diff --git a/src/cli/src/services.rs b/src/cli/src/services.rs deleted file mode 100644 index 4749ee9..0000000 --- a/src/cli/src/services.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::utils::ConfigFileHandler; -use reqwest::Client; -use serde::Deserialize; -use shared_models::{ - BuildConfig, CloudletDtoRequest, CloudletShutdownResponse, Language, ServerConfig, -}; -use std::error::Error; - -#[derive(Deserialize, Debug)] -struct TomlConfig { - #[serde(rename = "workload-name")] - workload_name: String, - language: Language, - action: String, - server: ServerConfig, - build: BuildConfig, -} - -pub struct CloudletClient {} - -impl CloudletClient { - pub fn new_cloudlet_config(config: String) -> CloudletDtoRequest { - let config: TomlConfig = - toml::from_str(&config).expect("Error while parsing the config file"); - - let workload_name = config.workload_name; - let code: String = ConfigFileHandler::read_file(&config.build.source_code_path) - .expect("Error while reading the code file"); - - let language = config.language; - CloudletDtoRequest { - workload_name, - language, - code, - log_level: shared_models::LogLevel::INFO, - server: config.server, - build: config.build, - action: config.action, - } - } - - pub async fn run(request: CloudletDtoRequest) -> Result<(), Box> { - let client = Client::new(); - let json = serde_json::to_string(&request)?; - let res = client - .post("http://127.0.0.1:3000/run") - .header(reqwest::header::CONTENT_TYPE, "application/json") - .body(json) - .send() - .await?; - - println!("Response: {:?}", res.text().await?); - Ok(()) - } - - pub async fn shutdown() -> Result { - let client = Client::new(); - let response = client.post("http://127.0.0.1:3000/shutdown").send().await; - - let shutdown_response: CloudletShutdownResponse = response - .unwrap() - .json::() - .await - .unwrap(); - - Ok(shutdown_response.success) - } -} diff --git a/src/cli/src/utils.rs b/src/cli/src/utils.rs index cb663ad..a77ef8d 100644 --- a/src/cli/src/utils.rs +++ b/src/cli/src/utils.rs @@ -1,14 +1,11 @@ use std::fs::File; -use std::io::{self, Read}; +use std::io::Read; use std::path::PathBuf; -pub struct ConfigFileHandler {} +pub fn read_file(file_path: &PathBuf) -> std::io::Result { + let mut file = File::open(file_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; -impl ConfigFileHandler { - pub fn read_file(file_path: &PathBuf) -> io::Result { - let mut file = File::open(file_path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(contents) - } + Ok(contents) }