Skip to content

Commit 1914eda

Browse files
committed
refactor: improve E2E test infrastructure with better error handling and modular cleanup
- Extract cleanup functionality into dedicated module - Add pre-flight cleanup to handle lingering resources from interrupted tests - Replace generic Box<dyn Error> with concrete EmergencyDestroyError type - Improve E2E test documentation with comprehensive error information - Add proper error handling traits and better resource conflict detection - Restructure imports for better separation of concerns
1 parent df83e3c commit 1914eda

File tree

6 files changed

+247
-45
lines changed

6 files changed

+247
-45
lines changed

src/bin/e2e_tests.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ use tracing::{error, info};
77
// Import E2E testing infrastructure
88
use torrust_tracker_deploy::e2e::environment::TestEnvironment;
99
use torrust_tracker_deploy::e2e::tasks::{
10+
cleanup_infrastructure::cleanup_infrastructure,
1011
configure_infrastructure::configure_infrastructure,
11-
provision_infrastructure::{cleanup_infrastructure, provision_infrastructure},
12-
validate_deployment::validate_deployment,
12+
preflight_cleanup::cleanup_lingering_resources,
13+
provision_infrastructure::provision_infrastructure, validate_deployment::validate_deployment,
1314
};
1415
use torrust_tracker_deploy::logging::{self, LogFormat};
1516

@@ -34,8 +35,26 @@ struct Cli {
3435
log_format: LogFormat,
3536
}
3637

38+
/// Main entry point for E2E tests.
39+
///
40+
/// Runs the full deployment workflow: provision infrastructure, configure services,
41+
/// validate deployment, and cleanup resources.
42+
///
43+
/// # Errors
44+
///
45+
/// Returns an error if:
46+
/// - Pre-flight cleanup fails
47+
/// - Infrastructure provisioning fails
48+
/// - Service configuration fails
49+
/// - Deployment validation fails
50+
/// - Resource cleanup fails (when enabled)
51+
///
52+
/// # Panics
53+
///
54+
/// May panic during the match statement if unexpected error combinations occur
55+
/// that are not handled by the current error handling logic.
3756
#[tokio::main]
38-
async fn main() -> Result<()> {
57+
pub async fn main() -> Result<()> {
3958
let cli = Cli::parse();
4059

4160
// Initialize logging based on the chosen format
@@ -50,6 +69,9 @@ async fn main() -> Result<()> {
5069

5170
let env = TestEnvironment::new(cli.keep, cli.templates_dir)?;
5271

72+
// Perform pre-flight cleanup to remove any lingering resources from interrupted tests
73+
cleanup_lingering_resources(&env)?;
74+
5375
let test_start = Instant::now();
5476

5577
let deployment_result = run_full_deployment_test(&env).await;

src/command_wrappers/opentofu/mod.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,38 @@ pub mod json_parser;
77
pub use client::{InstanceInfo, OpenTofuClient, OpenTofuError};
88
pub use json_parser::ParseError;
99

10+
/// Errors that can occur during emergency destroy operations
11+
#[derive(Debug)]
12+
pub enum EmergencyDestroyError {
13+
/// Command execution failed (e.g., tofu binary not found)
14+
CommandExecution { source: std::io::Error },
15+
16+
/// `OpenTofu` destroy operation failed with error output
17+
DestroyFailed { stderr: String },
18+
}
19+
20+
impl std::fmt::Display for EmergencyDestroyError {
21+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22+
match self {
23+
Self::CommandExecution { source } => {
24+
write!(f, "Failed to execute OpenTofu destroy command: {source}")
25+
}
26+
Self::DestroyFailed { stderr } => {
27+
write!(f, "OpenTofu destroy failed: {stderr}")
28+
}
29+
}
30+
}
31+
}
32+
33+
impl std::error::Error for EmergencyDestroyError {
34+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
35+
match self {
36+
Self::CommandExecution { source } => Some(source),
37+
Self::DestroyFailed { .. } => None,
38+
}
39+
}
40+
}
41+
1042
/// Emergency destroy operation for cleanup scenarios
1143
///
1244
/// This function performs a destructive `OpenTofu` destroy operation without prompting.
@@ -19,13 +51,13 @@ pub use json_parser::ParseError;
1951
///
2052
/// # Returns
2153
///
22-
/// * `Result<(), Box<dyn std::error::Error>>` - Success or error from the destroy operation
54+
/// * `Result<(), EmergencyDestroyError>` - Success or concrete error from the destroy operation
2355
///
2456
/// # Errors
2557
///
2658
/// Returns an error if the `OpenTofu` destroy command fails or if there are issues
2759
/// with command execution.
28-
pub fn emergency_destroy<P: AsRef<Path>>(working_dir: P) -> Result<(), Box<dyn std::error::Error>> {
60+
pub fn emergency_destroy<P: AsRef<Path>>(working_dir: P) -> Result<(), EmergencyDestroyError> {
2961
use std::process::Command;
3062

3163
tracing::debug!(
@@ -36,14 +68,15 @@ pub fn emergency_destroy<P: AsRef<Path>>(working_dir: P) -> Result<(), Box<dyn s
3668
let output = Command::new("tofu")
3769
.args(["destroy", "-auto-approve"])
3870
.current_dir(&working_dir)
39-
.output()?;
71+
.output()
72+
.map_err(|source| EmergencyDestroyError::CommandExecution { source })?;
4073

4174
if output.status.success() {
4275
tracing::debug!("Emergency destroy: `OpenTofu` destroy completed successfully");
4376
Ok(())
4477
} else {
45-
let stderr = String::from_utf8_lossy(&output.stderr);
78+
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
4679
tracing::error!("Emergency destroy: `OpenTofu` destroy failed: {stderr}");
47-
Err(format!("`OpenTofu` destroy failed: {stderr}").into())
80+
Err(EmergencyDestroyError::DestroyFailed { stderr })
4881
}
4982
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use tracing::{info, warn};
2+
3+
use crate::e2e::environment::TestEnvironment;
4+
5+
/// Clean up test infrastructure
6+
///
7+
/// This function destroys the test infrastructure using `OpenTofu`.
8+
/// If `keep_env` is set in the environment configuration, the cleanup
9+
/// is skipped and the environment is preserved.
10+
///
11+
/// # Arguments
12+
///
13+
/// * `env` - The test environment containing configuration and services
14+
///
15+
/// # Behavior
16+
///
17+
/// - If `env.config.keep_env` is `true`, logs a message and returns without cleanup
18+
/// - Otherwise, attempts to destroy infrastructure using `OpenTofu`
19+
/// - Logs success or failure appropriately
20+
/// - Does not return errors - failures are logged as warnings
21+
pub fn cleanup_infrastructure(env: &TestEnvironment) {
22+
if env.config.keep_env {
23+
info!(
24+
operation = "cleanup",
25+
action = "keep_environment",
26+
instance = "torrust-vm",
27+
connect_command = "lxc exec torrust-vm -- /bin/bash",
28+
"Keeping test environment as requested"
29+
);
30+
return;
31+
}
32+
33+
info!(operation = "cleanup", "Cleaning up test environment");
34+
35+
// Destroy infrastructure using OpenTofuClient
36+
let result = env
37+
.services
38+
.opentofu_client
39+
.destroy(true) // auto_approve = true
40+
.map_err(anyhow::Error::from);
41+
42+
match result {
43+
Ok(_) => info!(
44+
operation = "cleanup",
45+
status = "success",
46+
"Test environment cleaned up successfully"
47+
),
48+
Err(e) => warn!(
49+
operation = "cleanup",
50+
status = "failed",
51+
error = %e,
52+
"Cleanup failed"
53+
),
54+
}
55+
}

src/e2e/tasks/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod clean_and_prepare_templates;
2+
pub mod cleanup_infrastructure;
23
pub mod configure_infrastructure;
4+
pub mod preflight_cleanup;
35
pub mod provision_infrastructure;
46
pub mod setup_ssh_key;
57
pub mod validate_deployment;

src/e2e/tasks/preflight_cleanup.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//! Pre-flight cleanup module for E2E tests
2+
//!
3+
//! This module provides functionality to clean up any lingering resources
4+
//! from previous test runs that may have been interrupted before cleanup.
5+
6+
use std::fmt;
7+
use tracing::{info, warn};
8+
9+
use crate::command_wrappers::opentofu::{self, EmergencyDestroyError};
10+
use crate::e2e::environment::TestEnvironment;
11+
12+
/// Errors that can occur during pre-flight cleanup operations
13+
#[derive(Debug)]
14+
pub enum PreflightCleanupError {
15+
/// Emergency destroy operation failed
16+
EmergencyDestroyFailed { source: EmergencyDestroyError },
17+
18+
/// Resource conflicts detected that would prevent new test runs
19+
ResourceConflicts { details: String },
20+
}
21+
22+
impl fmt::Display for PreflightCleanupError {
23+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24+
match self {
25+
Self::EmergencyDestroyFailed { source } => {
26+
write!(f, "Emergency destroy operation failed: {source}")
27+
}
28+
Self::ResourceConflicts { details } => {
29+
write!(
30+
f,
31+
"Resource conflicts detected that would prevent new test runs: {details}"
32+
)
33+
}
34+
}
35+
}
36+
}
37+
38+
impl std::error::Error for PreflightCleanupError {
39+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
40+
match self {
41+
Self::EmergencyDestroyFailed { source } => Some(source),
42+
Self::ResourceConflicts { .. } => None,
43+
}
44+
}
45+
}
46+
47+
/// Performs pre-flight cleanup of any lingering test resources
48+
///
49+
/// This function attempts to clean up any resources that might remain from
50+
/// previous interrupted test runs. It's designed to be safe to run even when
51+
/// no resources exist.
52+
///
53+
/// # Arguments
54+
///
55+
/// * `env` - The test environment containing configuration and services
56+
///
57+
/// # Returns
58+
///
59+
/// Returns `Ok(())` if cleanup succeeds or if there were no resources to clean up.
60+
///
61+
/// # Errors
62+
///
63+
/// Returns an error if cleanup fails and resources are still present that would
64+
/// prevent new test runs from starting successfully.
65+
///
66+
/// # Examples
67+
///
68+
/// ```no_run
69+
/// use torrust_tracker_deploy::e2e::{environment::TestEnvironment, tasks::preflight_cleanup};
70+
///
71+
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
72+
/// let env = TestEnvironment::new(false, "./templates")?;
73+
/// preflight_cleanup::cleanup_lingering_resources(&env)?;
74+
/// # Ok(())
75+
/// # }
76+
/// ```
77+
pub fn cleanup_lingering_resources(env: &TestEnvironment) -> Result<(), PreflightCleanupError> {
78+
info!(
79+
operation = "preflight_cleanup",
80+
"Starting pre-flight cleanup of any lingering test resources"
81+
);
82+
83+
// Attempt to destroy any existing OpenTofu infrastructure
84+
let tofu_dir = env.config.build_dir.join(&env.config.opentofu_subfolder);
85+
86+
if !tofu_dir.exists() {
87+
info!(
88+
operation = "preflight_cleanup",
89+
status = "clean",
90+
"No OpenTofu directory found, skipping cleanup"
91+
);
92+
return Ok(());
93+
}
94+
95+
// Use emergency_destroy which is designed to handle cases where resources may not exist
96+
match opentofu::emergency_destroy(&tofu_dir) {
97+
Ok(()) => {
98+
info!(
99+
operation = "preflight_cleanup",
100+
status = "success",
101+
"Pre-flight cleanup completed successfully"
102+
);
103+
}
104+
Err(e) => {
105+
// Log as warning rather than error since this is pre-flight cleanup
106+
// and resources may legitimately not exist
107+
warn!(
108+
operation = "preflight_cleanup",
109+
status = "partial_failure",
110+
error = %e,
111+
"Pre-flight cleanup encountered issues (this may be normal if no resources existed)"
112+
);
113+
114+
// Don't return an error for pre-flight cleanup failures unless they indicate
115+
// actual resource conflicts that would prevent new test runs
116+
let error_message = e.to_string().to_lowercase();
117+
if error_message.contains("already exists") || error_message.contains("in use") {
118+
return Err(PreflightCleanupError::ResourceConflicts {
119+
details: e.to_string(),
120+
});
121+
}
122+
return Err(PreflightCleanupError::EmergencyDestroyFailed { source: e });
123+
}
124+
}
125+
Ok(())
126+
}
Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::{Context, Result};
22
use std::net::IpAddr;
33
use std::sync::Arc;
4-
use tracing::{info, warn};
4+
use tracing::info;
55

66
use crate::commands::ProvisionCommand;
77
use crate::e2e::environment::TestEnvironment;
@@ -41,39 +41,3 @@ pub async fn provision_infrastructure(env: &TestEnvironment) -> Result<IpAddr> {
4141
// Return the IP from OpenTofu as it's our preferred source
4242
Ok(opentofu_instance_ip)
4343
}
44-
45-
pub fn cleanup_infrastructure(env: &TestEnvironment) {
46-
if env.config.keep_env {
47-
info!(
48-
operation = "cleanup",
49-
action = "keep_environment",
50-
instance = "torrust-vm",
51-
connect_command = "lxc exec torrust-vm -- /bin/bash",
52-
"Keeping test environment as requested"
53-
);
54-
return;
55-
}
56-
57-
info!(operation = "cleanup", "Cleaning up test environment");
58-
59-
// Destroy infrastructure using OpenTofuClient
60-
let result = env
61-
.services
62-
.opentofu_client
63-
.destroy(true) // auto_approve = true
64-
.map_err(anyhow::Error::from);
65-
66-
match result {
67-
Ok(_) => info!(
68-
operation = "cleanup",
69-
status = "success",
70-
"Test environment cleaned up successfully"
71-
),
72-
Err(e) => warn!(
73-
operation = "cleanup",
74-
status = "failed",
75-
error = %e,
76-
"Cleanup failed"
77-
),
78-
}
79-
}

0 commit comments

Comments
 (0)