use std::net::SocketAddr;
use std::time::Instant;
use anyhow::{Context, Result};
use clap::Parser;
use torrust_tracker_deployer_dependency_installer::Dependency;
use tracing::{error, info};
use torrust_tracker_deployer_lib::adapters::ssh::SshCredentials;
use torrust_tracker_deployer_lib::bootstrap::logging::{LogFormat, LogOutput, LoggingBuilder};
use torrust_tracker_deployer_lib::shared::{ServiceEndpoint, Username};
use torrust_tracker_deployer_lib::testing::e2e::containers::actions::{
SshKeySetupAction, SshWaitAction,
};
use torrust_tracker_deployer_lib::testing::e2e::containers::timeout::ContainerTimeouts;
use torrust_tracker_deployer_lib::testing::e2e::containers::tracker_container_setup::{
ContainerPorts, E2eConfigEnvironment, E2eRuntimeEnvironment,
};
use torrust_tracker_deployer_lib::testing::e2e::containers::{
RunningProvisionedContainer, StoppedProvisionedContainer,
};
use torrust_tracker_deployer_lib::testing::e2e::tasks::black_box::{
build_e2e_test_config, run_container_preflight_cleanup, verify_required_dependencies,
write_environment_config, E2eTestRunner,
};
use torrust_tracker_deployer_lib::testing::e2e::tasks::container::cleanup_infrastructure::stop_test_infrastructure;
use torrust_tracker_deployer_lib::testing::e2e::tasks::run_configuration_validation::run_configuration_validation;
use torrust_tracker_deployer_lib::testing::e2e::tasks::run_release_validation::{
run_release_validation, ServiceValidation,
};
use torrust_tracker_deployer_lib::testing::e2e::tasks::run_run_validation::{
run_run_validation, ServiceValidation as RunServiceValidation,
};
const ENVIRONMENT_NAME: &str = "e2e-deployment";
fn cleanup_test_artifacts(environment_name: &str) -> Result<()> {
run_container_preflight_cleanup(environment_name)
}
#[derive(Parser)]
#[command(name = "e2e-deployment-workflow-tests")]
#[command(about = "E2E deployment workflow tests using black-box approach with Docker containers")]
struct CliArgs {
#[arg(
long,
default_value = "pretty",
help = "Logging format: pretty, json, or compact"
)]
log_format: LogFormat,
}
#[tokio::main]
pub async fn main() -> Result<()> {
let cli = CliArgs::parse();
unsafe {
std::env::set_var("TORRUST_TD_SKIP_FIREWALL_IN_CONTAINER", "true");
std::env::set_var("TORRUST_TD_SKIP_DOCKER_INSTALL_IN_CONTAINER", "true");
}
LoggingBuilder::new(std::path::Path::new("./data/e2e-deployment/logs"))
.with_format(cli.log_format.clone())
.with_output(LogOutput::FileAndStderr)
.init();
info!(
application = "torrust_tracker_deployer",
test_suite = "e2e_deployment_workflow_tests",
log_format = ?cli.log_format,
"Starting E2E configuration and release tests (black-box) with Docker containers"
);
verify_required_dependencies(&[Dependency::Ansible])?;
let test_start = Instant::now();
run_container_preflight_cleanup(ENVIRONMENT_NAME)?;
let test_result = run_configure_release_run_tests().await;
let test_duration = test_start.elapsed();
info!(
performance = "test_execution",
duration_secs = test_duration.as_secs_f64(),
duration = ?test_duration,
"Configuration test execution completed"
);
match test_result {
Ok(()) => {
info!(
test_suite = "e2e_deployment_workflow_tests",
status = "success",
"All configuration and release tests passed successfully"
);
if let Err(cleanup_error) = cleanup_test_artifacts(ENVIRONMENT_NAME) {
tracing::warn!(
operation = "post_test_cleanup",
environment = ENVIRONMENT_NAME,
error = %cleanup_error,
"Failed to clean up test state after successful run (test still passed)"
);
} else {
info!(
operation = "post_test_cleanup",
environment = ENVIRONMENT_NAME,
"Test state cleaned up successfully"
);
}
Ok(())
}
Err(error) => {
error!(
test_suite = "e2e_deployment_workflow_tests",
status = "failed",
error = %error,
"Configuration and release tests failed"
);
Err(error)
}
}
}
async fn run_configure_release_run_tests() -> Result<()> {
info!("Starting configure → release → run tests with Docker container (black-box approach)");
let ssh_credentials = build_test_ssh_credentials();
let config_env = build_e2e_test_config(ENVIRONMENT_NAME);
let (runtime_env, running_container) = create_and_start_container(&config_env).await?;
let ssh_socket_address = runtime_env.ssh_socket_addr();
establish_ssh_connectivity(
ssh_socket_address,
&ssh_credentials,
Some(&running_container),
)
.await?;
let test_result = run_deployer_workflow(&config_env, &runtime_env, &ssh_credentials).await;
stop_test_infrastructure(running_container);
test_result
}
async fn run_deployer_workflow(
config_env: &E2eConfigEnvironment,
runtime_env: &E2eRuntimeEnvironment,
ssh_credentials: &SshCredentials,
) -> Result<()> {
let test_runner = E2eTestRunner::new(ENVIRONMENT_NAME);
write_environment_config(config_env)?;
test_runner.create_environment(&config_env.config_file_path)?;
let socket_addr = runtime_env.ssh_socket_addr();
let instance_ip = socket_addr.ip().to_string();
let ssh_port = runtime_env.container_ports.ssh_port;
test_runner.register_instance(&instance_ip, Some(ssh_port))?;
test_runner.configure_services()?;
run_configuration_validation(socket_addr, ssh_credentials)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
test_runner.release_software()?;
let services = ServiceValidation {
prometheus: true,
grafana: true,
};
run_release_validation(socket_addr, ssh_credentials, Some(services))
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
test_runner.run_services()?;
let run_services = RunServiceValidation {
prometheus: true,
grafana: true,
};
let server_ip = socket_addr.ip();
let api_endpoint = ServiceEndpoint::http(
std::net::SocketAddr::new(server_ip, runtime_env.container_ports.http_api_port),
"/api/health_check",
)
.expect("Valid socket address should produce valid HTTP URL");
let http_tracker_endpoint = ServiceEndpoint::http(
std::net::SocketAddr::new(server_ip, runtime_env.container_ports.http_tracker_port),
"/health_check",
)
.expect("Valid socket address should produce valid HTTP URL");
run_run_validation(
socket_addr,
ssh_credentials,
api_endpoint,
vec![http_tracker_endpoint],
Some(run_services),
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
info!("Configure → release → run workflow tests completed successfully");
Ok(())
}
fn build_test_ssh_credentials() -> SshCredentials {
let project_root = std::env::current_dir().expect("Failed to get current directory");
let ssh_private_key_path = project_root.join("fixtures/testing_rsa");
let ssh_public_key_path = project_root.join("fixtures/testing_rsa.pub");
let ssh_user = Username::new("torrust").expect("Valid hardcoded username");
SshCredentials::new(ssh_private_key_path, ssh_public_key_path, ssh_user)
}
async fn create_and_start_container(
config_env: &E2eConfigEnvironment,
) -> Result<(E2eRuntimeEnvironment, RunningProvisionedContainer)> {
let additional_ports = config_env.tracker_ports.all_ports();
info!(
environment_name = %config_env.environment_name,
ssh_port = %config_env.ssh_port,
http_api_port = config_env.tracker_ports.http_api_port,
http_tracker_port = config_env.tracker_ports.http_tracker_port,
udp_tracker_port = config_env.tracker_ports.udp_tracker_port,
"Creating and starting Docker container for E2E testing with tracker ports from config"
);
let stopped_container = StoppedProvisionedContainer::default();
let running_container = stopped_container
.start(
Some(config_env.environment_name.clone()),
config_env.ssh_port,
&additional_ports,
)
.await
.context("Failed to start provisioned instance container")?;
let ssh_mapped_port = running_container.ssh_socket_addr().port();
let additional_mapped_ports = running_container.additional_mapped_ports();
let container_ports =
ContainerPorts::from_mapped_ports(ssh_mapped_port, additional_mapped_ports);
let runtime_env = E2eRuntimeEnvironment::new(config_env.clone(), container_ports);
info!(
environment_name = %config_env.environment_name,
container_id = %running_container.container_id(),
ssh_socket_addr = %running_container.ssh_socket_addr(),
"Docker container setup completed successfully"
);
Ok((runtime_env, running_container))
}
async fn establish_ssh_connectivity(
socket_addr: SocketAddr,
ssh_credentials: &SshCredentials,
container: Option<&RunningProvisionedContainer>,
) -> Result<()> {
info!(
socket_addr = %socket_addr,
ssh_user = %ssh_credentials.ssh_username,
"Establishing SSH connectivity"
);
let timeouts = ContainerTimeouts::default();
let ssh_wait_action = SshWaitAction::new(timeouts.ssh_ready, 10);
ssh_wait_action
.execute(socket_addr)
.context("SSH server failed to start")?;
if let Some(running_container) = container {
let ssh_key_setup_action = SshKeySetupAction::new();
ssh_key_setup_action
.execute(running_container, ssh_credentials)
.await
.context("Failed to setup SSH authentication")?;
}
info!(
socket_addr = %socket_addr,
ssh_user = %ssh_credentials.ssh_username,
"SSH connectivity established successfully"
);
Ok(())
}