Skip to content

Commit

Permalink
ci: [torrust#634] E2E test runner: parse log to get running services
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jan 23, 2024
1 parent 3f5bc44 commit 951ae36
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 21 deletions.
71 changes: 53 additions & 18 deletions src/e2e/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Output> {
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<Output> {
let initial_args = vec![
"run".to_string(),
"--detach".to_string(),
"--name".to_string(),
container.to_string(),
];

let mut env_var_args: Vec<String> = 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}"),
))
}
}

Expand All @@ -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<Child> {
pub fn run_spawned(image: &str, container: &str) -> io::Result<Child> {
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
Expand All @@ -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}"),
))
}
}

Expand All @@ -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}"),
))
}
}

Expand All @@ -91,15 +126,15 @@ impl Docker {
/// # Errors
///
/// Will fail if the docker logs command fails.
pub fn logs(container_name: &str) -> io::Result<String> {
let output = Command::new("docker").args(["logs", container_name]).output()?;
pub fn logs(container: &str) -> io::Result<String> {
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}"),
))
}
}
Expand Down
34 changes: 34 additions & 0 deletions src/e2e/logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct RunningServices {
pub udp_trackers: Vec<String>,
pub http_trackers: Vec<String>,
pub health_checks: Vec<String>,
}

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")
}
}
1 change: 1 addition & 0 deletions src/e2e/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod docker;
pub mod logs;
pub mod runner;
pub mod temp_dir;
77 changes: 74 additions & 3 deletions src/e2e/runner.rs
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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: {:?}",
Expand All @@ -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));
Expand All @@ -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");

Expand All @@ -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<String> = std::env::args().collect();

if args.len() < NUMBER_OF_ARGUMENTS {
eprintln!("Usage: cargo run --bin e2e_tests_runner <PATH_TO_TRACKER_CONFIG_FILE>");
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)
Expand Down

0 comments on commit 951ae36

Please sign in to comment.