diff --git a/src/e2e/docker.rs b/src/e2e/docker.rs index 6770cec8..6806d789 100644 --- a/src/e2e/docker.rs +++ b/src/e2e/docker.rs @@ -3,6 +3,8 @@ use std::process::{Child, Command, Output, Stdio}; use std::thread::sleep; use std::time::{Duration, Instant}; +use log::debug; + pub struct Docker {} impl Docker { @@ -19,24 +21,51 @@ impl Docker { if status.success() { Ok(()) } else { - Err(io::Error::new(io::ErrorKind::Other, "Failed to build Docker image")) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to build Docker image from dockerfile {dockerfile}"), + )) } } - /// Runs a Docker container from a given image. + /// Runs a Docker container from a given image with multiple environment variables. + /// + /// # Arguments + /// + /// * `image` - The Docker image to run. + /// * `container` - The name for the Docker container. + /// * `env_vars` - A slice of tuples, each representing an environment variable as ("KEY", "value"). /// /// # Errors /// /// Will fail if the docker run command fails. - pub fn run(image: &str, name: &str) -> io::Result { - let output = Command::new("docker") - .args(["run", "--detach", "--name", name, image]) - .output()?; + pub fn run(image: &str, container: &str, env_vars: &[(String, String)]) -> io::Result { + let initial_args = vec![ + "run".to_string(), + "--detach".to_string(), + "--name".to_string(), + container.to_string(), + ]; + + let mut env_var_args: Vec = vec![]; + for (key, value) in env_vars { + env_var_args.push("--env".to_string()); + env_var_args.push(format!("{key}={value}")); + } + + let args = [initial_args, env_var_args, [image.to_string()].to_vec()].concat(); + + debug!("Docker run args: {:?}", args); + + let output = Command::new("docker").args(args).output()?; if output.status.success() { Ok(output) } else { - Err(io::Error::new(io::ErrorKind::Other, "Failed to run Docker container")) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to run Docker image {image}"), + )) } } @@ -45,9 +74,9 @@ impl Docker { /// # Errors /// /// Will fail if the docker run command fails to start. - pub fn run_spawned(image: &str, name: &str) -> io::Result { + pub fn run_spawned(image: &str, container: &str) -> io::Result { let child = Command::new("docker") - .args(["run", "--name", name, image]) + .args(["run", "--name", container, image]) .stdin(Stdio::null()) // Ignore stdin .stdout(Stdio::null()) // Ignore stdout .stderr(Stdio::null()) // Ignore stderr @@ -61,13 +90,16 @@ impl Docker { /// # Errors /// /// Will fail if the docker stop command fails. - pub fn stop(name: &str) -> io::Result<()> { - let status = Command::new("docker").args(["stop", name]).status()?; + pub fn stop(container: &str) -> io::Result<()> { + let status = Command::new("docker").args(["stop", container]).status()?; if status.success() { Ok(()) } else { - Err(io::Error::new(io::ErrorKind::Other, "Failed to stop Docker container")) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to stop Docker container {container}"), + )) } } @@ -76,13 +108,16 @@ impl Docker { /// # Errors /// /// Will fail if the docker rm command fails. - pub fn remove(name: &str) -> io::Result<()> { - let status = Command::new("docker").args(["rm", "-f", name]).status()?; + pub fn remove(container: &str) -> io::Result<()> { + let status = Command::new("docker").args(["rm", "-f", container]).status()?; if status.success() { Ok(()) } else { - Err(io::Error::new(io::ErrorKind::Other, "Failed to remove Docker container")) + Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to remove Docker container {container}"), + )) } } @@ -91,15 +126,15 @@ impl Docker { /// # Errors /// /// Will fail if the docker logs command fails. - pub fn logs(container_name: &str) -> io::Result { - let output = Command::new("docker").args(["logs", container_name]).output()?; + pub fn logs(container: &str) -> io::Result { + let output = Command::new("docker").args(["logs", container]).output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(io::Error::new( io::ErrorKind::Other, - "Failed to fetch logs from Docker container", + format!("Failed to fetch logs from Docker container {container}"), )) } } diff --git a/src/e2e/logs.rs b/src/e2e/logs.rs new file mode 100644 index 00000000..98219a9c --- /dev/null +++ b/src/e2e/logs.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct RunningServices { + pub udp_trackers: Vec, + pub http_trackers: Vec, + pub health_checks: Vec, +} + +impl RunningServices { + pub fn extract_from_logs(&mut self, logs: &str) { + // todo: extract duplicate code + for line in logs.lines() { + let heal_check_pattern = "[Health Check API][INFO] Starting on: "; + let udp_tracker_pattern = "[UDP Tracker][INFO] Starting on: udp://"; + let http_tracker_pattern = "[HTTP Tracker][INFO] Starting on: "; + + if let Some(start) = line.find(heal_check_pattern) { + let address = &line[start + heal_check_pattern.len()..].trim(); + self.health_checks.push(Self::replace_wildcard_ip_with_localhost(address)); + } else if let Some(start) = line.find(udp_tracker_pattern) { + let address = &line[start + udp_tracker_pattern.len()..].trim(); + self.udp_trackers.push(Self::replace_wildcard_ip_with_localhost(address)); + } else if let Some(start) = line.find(http_tracker_pattern) { + let address = &line[start + http_tracker_pattern.len()..].trim(); + self.http_trackers.push(Self::replace_wildcard_ip_with_localhost(address)); + } + } + } + + fn replace_wildcard_ip_with_localhost(address: &str) -> String { + address.replace("0.0.0.0", "127.0.0.1") + } +} diff --git a/src/e2e/mod.rs b/src/e2e/mod.rs index bc8a6d8d..7c9190a5 100644 --- a/src/e2e/mod.rs +++ b/src/e2e/mod.rs @@ -1,3 +1,4 @@ pub mod docker; +pub mod logs; pub mod runner; pub mod temp_dir; diff --git a/src/e2e/runner.rs b/src/e2e/runner.rs index 3a7e1c86..30ae2622 100644 --- a/src/e2e/runner.rs +++ b/src/e2e/runner.rs @@ -1,12 +1,16 @@ use std::env; use std::time::Duration; +use log::{debug, LevelFilter}; use rand::distributions::Alphanumeric; use rand::Rng; use crate::e2e::docker::Docker; +use crate::e2e::logs::RunningServices; use crate::e2e::temp_dir::Handler; +pub const NUMBER_OF_ARGUMENTS: usize = 2; + /// # Panics /// /// Will panic if: @@ -21,14 +25,24 @@ pub fn run() { - [x] Build the docker image. - [x] Run the docker image. - [x] Wait until the container is healthy. - - [ ] Parse logs to get running services. + - [x] Parse logs to get running services. - [ ] Build config file for the tracker_checker. - [ ] Run the tracker_checker. - [x] Stop the container. */ - Docker::build("./Containerfile", "local").expect("A tracker local docker image should be built"); + setup_logging(LevelFilter::Debug); + + let args = parse_arguments(); + + println!("Reading tracker configuration from file: {} ...", args.tracker_config_path); + + let tracker_config = read_tracker_config(&args.tracker_config_path); + + let container_tag = "torrust-tracker:local"; + + Docker::build("./Containerfile", container_tag).expect("A tracker local docker image should be built"); println!( "Current dir: {:?}", @@ -47,7 +61,8 @@ pub fn run() { let container_name = generate_random_container_name("tracker_"); println!("Running docker tracker image: {container_name} ..."); - Docker::run("local", &container_name).expect("A tracker local docker image should be running"); + let env_vars = [("TORRUST_TRACKER_CONFIG".to_string(), tracker_config.to_string())]; + Docker::run(container_tag, &container_name, &env_vars).expect("A tracker local docker image should be running"); println!("Waiting for the container {container_name} to be healthy ..."); let is_healthy = Docker::wait_until_is_healthy(&container_name, Duration::from_secs(10)); @@ -61,6 +76,16 @@ pub fn run() { println!("Container {container_name} is healthy ..."); + let logs = Docker::logs(&container_name).expect("Logs should be captured from running container"); + + debug!("Logs after starting the container: {logs}"); + + let mut config = RunningServices::default(); + config.extract_from_logs(&logs); + + let json = serde_json::to_string_pretty(&config).unwrap(); + println!("Tracker checker configuration: {json}"); + println!("Stopping docker tracker image: {container_name} ..."); Docker::stop(&container_name).expect("A tracker local docker image should be stopped"); @@ -70,6 +95,52 @@ pub fn run() { .expect("The app should revert dir from temp dir to the original one"); } +fn setup_logging(level: LevelFilter) { + if let Err(_err) = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} [{}][{}] {}", + chrono::Local::now().format("%+"), + record.target(), + record.level(), + message + )); + }) + .level(level) + .chain(std::io::stdout()) + .apply() + { + panic!("Failed to initialize logging.") + } + + debug!("logging initialized."); +} + +pub struct Arguments { + pub tracker_config_path: String, +} + +fn parse_arguments() -> Arguments { + let args: Vec = std::env::args().collect(); + + if args.len() < NUMBER_OF_ARGUMENTS { + eprintln!("Usage: cargo run --bin e2e_tests_runner "); + eprintln!("For example: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml"); + std::process::exit(1); + } + + let config_path = &args[1]; + + Arguments { + tracker_config_path: config_path.to_string(), + } +} + +fn read_tracker_config(tracker_config_path: &str) -> String { + std::fs::read_to_string(tracker_config_path) + .unwrap_or_else(|_| panic!("Can't read tracker config file {tracker_config_path}")) +} + fn generate_random_container_name(prefix: &str) -> String { let rand_string: String = rand::thread_rng() .sample_iter(&Alphanumeric)