Skip to content

Commit 18f7ce2

Browse files
author
Mateo Fernandez
committed
refactor(cli): use SSE for streaming logs
Signed-off-by: Mateo Fernandez <mateo.fernandez@etu.umontpellier.fr>
1 parent 9ab2b92 commit 18f7ce2

12 files changed

Lines changed: 200 additions & 140 deletions

File tree

src/cli/Cargo.toml

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ name = "cli"
33
version = "0.1.0"
44
edition = "2021"
55

6-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7-
86
[dependencies]
9-
clap = { version = "4.5.3", features = ["derive"] }
10-
toml = "0.8.12"
11-
tokio = { version = "1.36.0", features = ["full"] }
12-
serde = { version = "1.0.197", features = ["derive"] }
13-
serde_yaml = "0.9.34"
14-
schemars = "0.8.16"
15-
serde_json = "1.0.115"
16-
reqwest = "0.12.3"
17-
shared_models = { path="../shared-models" }
7+
clap = { version = "4.5", features = ["derive"] }
8+
crossterm = "0.27"
9+
futures = "0.3"
10+
reqwest = { version = "0.12", features = ["json"] }
11+
reqwest-eventsource = "0.6"
12+
serde = { version = "1.0", features = ["derive"] }
13+
serde_json = "1.0"
14+
shared_models = { path = "../shared-models" }
15+
tokio = { version = "1.38", features = ["full"] }
16+
toml = "0.8"

src/cli/config/config.template.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/cli/config/config.yaml

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/cli/config/example.env

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/cli/src/api_client/execute.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use super::Error;
2+
use reqwest_eventsource::EventSource;
3+
use serde::Deserialize;
4+
use shared_models::CloudletDtoRequest;
5+
6+
pub async fn execute(base_url: &str, request: CloudletDtoRequest) -> Result<EventSource, Error> {
7+
let client = reqwest::Client::new()
8+
.post(format!("{base_url}/run"))
9+
.json(&request);
10+
11+
EventSource::new(client).map_err(Error::CreateEventSource)
12+
}
13+
14+
#[derive(Debug, Deserialize)]
15+
pub struct ExecuteJsonResponse {
16+
pub stage: Stage,
17+
pub stdout: Option<String>,
18+
pub stderr: Option<String>,
19+
pub exit_code: Option<i32>,
20+
}
21+
22+
#[derive(Debug, Deserialize)]
23+
pub enum Stage {
24+
Pending,
25+
Building,
26+
Running,
27+
Done,
28+
Failed,
29+
Debug,
30+
}
31+
32+
impl TryFrom<String> for ExecuteJsonResponse {
33+
type Error = Error;
34+
35+
fn try_from(value: String) -> Result<Self, Self::Error> {
36+
serde_json::from_str(&value).map_err(|_| Error::ExecuteResponseDeserialize)
37+
}
38+
}

src/cli/src/api_client/mod.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use crate::utils;
2+
use serde::Deserialize;
3+
use shared_models::{BuildConfig, CloudletDtoRequest, Language, ServerConfig};
4+
use std::{fs, path::PathBuf};
5+
6+
pub mod execute;
7+
pub mod shutdown;
8+
9+
pub use execute::*;
10+
pub use shutdown::*;
11+
12+
#[derive(Debug)]
13+
pub enum Error {
14+
ReadTomlConfigFile(std::io::Error),
15+
TomlConfigParse(toml::de::Error),
16+
ReadCodeFile(std::io::Error),
17+
ExecuteRequestBody,
18+
CreateEventSource(reqwest_eventsource::CannotCloneRequestError),
19+
ExecuteResponseDeserialize,
20+
ShutdownSendRequest(reqwest::Error),
21+
ShutdownResponse(reqwest::Error),
22+
}
23+
24+
#[derive(Debug, Deserialize)]
25+
#[serde(rename_all = "kebab-case")]
26+
struct TomlConfig {
27+
workload_name: String,
28+
language: Language,
29+
action: String,
30+
server: ServerConfig,
31+
build: BuildConfig,
32+
}
33+
34+
pub fn new_cloudlet_request(config_path: &PathBuf) -> Result<CloudletDtoRequest, Error> {
35+
let toml_file = fs::read_to_string(config_path).map_err(Error::ReadTomlConfigFile)?;
36+
let config: TomlConfig = toml::from_str(&toml_file).map_err(Error::TomlConfigParse)?;
37+
38+
let source_code_path = &config.build.source_code_path;
39+
let code: String = utils::read_file(source_code_path).map_err(Error::ReadCodeFile)?;
40+
41+
Ok(CloudletDtoRequest {
42+
workload_name: config.workload_name,
43+
language: config.language,
44+
code,
45+
log_level: shared_models::LogLevel::INFO,
46+
server: config.server,
47+
build: config.build,
48+
action: config.action,
49+
})
50+
}

src/cli/src/api_client/shutdown.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use super::Error;
2+
use shared_models::CloudletShutdownResponse;
3+
4+
pub async fn shutdown(base_url: &str) -> Result<CloudletShutdownResponse, Error> {
5+
let client = reqwest::Client::new();
6+
7+
client
8+
.post(format!("{base_url}/shutdown"))
9+
.send()
10+
.await
11+
.map_err(Error::ShutdownSendRequest)?
12+
.json::<CloudletShutdownResponse>()
13+
.await
14+
.map_err(Error::ShutdownSendRequest)
15+
}

src/cli/src/args.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ pub enum Commands {
1414
#[arg(short, long)]
1515
config_path: PathBuf,
1616
},
17-
Shutdown {},
17+
Shutdown,
1818
}

src/cli/src/lib.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use api_client::ExecuteJsonResponse;
2+
use args::{CliArgs, Commands};
3+
use crossterm::style::Stylize;
4+
use futures::TryStreamExt;
5+
use reqwest_eventsource::Event;
6+
use std::fmt::Display;
7+
8+
mod api_client;
9+
pub mod args;
10+
mod utils;
11+
12+
#[derive(Debug)]
13+
pub enum Error {
14+
StdoutExecute(std::io::Error),
15+
ApiClient(api_client::Error),
16+
InvalidRequest(String),
17+
ProgramFailed,
18+
}
19+
20+
pub async fn run_cli(base_url: &str, args: CliArgs) -> Result<i32, Error> {
21+
match args.command {
22+
Commands::Run { config_path } => {
23+
let body = api_client::new_cloudlet_request(&config_path).map_err(Error::ApiClient)?;
24+
let mut es = api_client::execute(base_url, body)
25+
.await
26+
.map_err(Error::ApiClient)?;
27+
28+
let mut exit_code = 0;
29+
30+
while let Ok(Some(event)) = es.try_next().await {
31+
match event {
32+
Event::Open => { /* skip */ }
33+
Event::Message(msg) => {
34+
let exec_response = ExecuteJsonResponse::try_from(msg.data);
35+
if let Ok(exec_response) = exec_response {
36+
if let Some(stdout) = exec_response.stdout {
37+
println!("{}", stylize(stdout, &exec_response.stage));
38+
}
39+
if let Some(stderr) = exec_response.stderr {
40+
println!("{}", stylize(stderr, &exec_response.stage));
41+
}
42+
if let Some(code) = exec_response.exit_code {
43+
exit_code = code;
44+
}
45+
}
46+
}
47+
}
48+
}
49+
50+
Ok(exit_code)
51+
}
52+
Commands::Shutdown {} => {
53+
api_client::shutdown(base_url)
54+
.await
55+
.map_err(Error::ApiClient)?;
56+
57+
Ok(0)
58+
}
59+
}
60+
}
61+
62+
fn stylize(output: String, stage: &api_client::Stage) -> impl Display {
63+
match stage {
64+
api_client::Stage::Building => output.yellow(),
65+
api_client::Stage::Failed => output.dark_red(),
66+
api_client::Stage::Debug => output.dark_blue(),
67+
_ => output.stylize(),
68+
}
69+
}

src/cli/src/main.rs

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,20 @@
11
use clap::Parser;
2-
3-
use args::{CliArgs, Commands};
4-
5-
use services::CloudletClient;
6-
use std::{fs, io, process::exit};
7-
8-
mod args;
9-
mod services;
10-
mod utils;
2+
use cli::args::CliArgs;
3+
use std::process::exit;
114

125
#[tokio::main]
13-
async fn main() -> io::Result<()> {
6+
async fn main() {
147
let args = CliArgs::parse();
158

16-
match args.command {
17-
Commands::Run { config_path } => {
18-
let toml_file = match fs::read_to_string(config_path.clone()) {
19-
Ok(c) => c,
20-
Err(_) => {
21-
eprintln!("Could not read file `{:?}`", config_path);
22-
exit(1);
23-
}
24-
};
25-
let body = CloudletClient::new_cloudlet_config(toml_file);
26-
let response = CloudletClient::run(body).await;
9+
let api_url = std::env::var("API_URL").unwrap_or("localhost:3000".into());
10+
let api_url = format!("http://{api_url}");
2711

28-
match response {
29-
Ok(_) => println!("Request successful {:?}", response),
30-
Err(e) => eprintln!("Error while making the request: {}", e),
31-
}
32-
}
33-
Commands::Shutdown {} => {
34-
let response = CloudletClient::shutdown().await;
35-
match response {
36-
Ok(bool) => {
37-
if bool {
38-
println!("Shutdown Request successful !")
39-
} else {
40-
println!("Shutdown Request Failed")
41-
}
42-
}
43-
Err(()) => println!("Cannot send shutdown Request"),
44-
}
12+
let result = cli::run_cli(&api_url, args).await;
13+
match result {
14+
Ok(exit_code) => exit(exit_code),
15+
Err(e) => {
16+
eprintln!("Could not execute the command:\n{:?}", e);
17+
exit(1);
4518
}
4619
}
47-
48-
Ok(())
4920
}

0 commit comments

Comments
 (0)