Skip to main content

torrust_tracker_deployer_lib/testing/e2e/tasks/
preflight_cleanup.rs

1//! Generic preflight cleanup functionality
2//!
3//! This module provides directory cleanup functions that are used by both
4//! container-based and VM-based E2E testing workflows. These functions handle
5//! the cleanup of build and template directories to ensure test isolation.
6
7use std::fmt;
8
9use crate::adapters::tofu::EmergencyDestroyError;
10use crate::testing::e2e::context::TestContext;
11use tracing::{info, warn};
12
13// Re-export functions from the new modular structure for backward compatibility
14pub use crate::testing::e2e::tasks::container::preflight_cleanup::preflight_cleanup_previous_resources;
15
16/// Errors that can occur during pre-flight cleanup operations
17#[derive(Debug)]
18pub enum PreflightCleanupError {
19    /// Emergency destroy operation failed
20    EmergencyDestroyFailed { source: EmergencyDestroyError },
21
22    /// Resource conflicts detected that would prevent new test runs
23    ResourceConflicts { details: String },
24}
25
26impl fmt::Display for PreflightCleanupError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Self::EmergencyDestroyFailed { source } => {
30                write!(f, "Emergency destroy operation failed: {source}")
31            }
32            Self::ResourceConflicts { details } => {
33                write!(
34                    f,
35                    "Resource conflicts detected that would prevent new test runs: {details}"
36                )
37            }
38        }
39    }
40}
41
42impl std::error::Error for PreflightCleanupError {
43    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44        match self {
45            Self::EmergencyDestroyFailed { source } => Some(source),
46            Self::ResourceConflicts { .. } => None,
47        }
48    }
49}
50
51// TODO: Refactor TestContext to eliminate the need for this workaround function
52//
53// Current issue: TestContext requires an Environment, but we need to clean up data
54// directories BEFORE creating the Environment (because CreateCommandHandler checks
55// if the environment already exists in the repository).
56//
57// Proposed solutions:
58// 1. Make Environment optional in TestContext (TestContext { environment: Option<Environment> })
59// 2. Move Environment out of TestContext (preferred - better separation of concerns)
60//
61// The second option is better because:
62// - TestContext should manage test infrastructure (services, config, temp directories)
63// - Environment is a domain entity that represents deployment state
64// - Separating them provides clearer responsibilities and easier testing
65//
66// After refactoring, we could eliminate this standalone function and have all cleanup
67// go through a single preflight_cleanup_previous_resources() that doesn't require
68// a fully initialized TestContext with an Environment.
69
70/// Cleans the data directory for a specific environment name before `TestContext` creation
71///
72/// This helper function removes the `data/{environment_name}` directory to prevent
73/// "environment already exists" errors when `CreateCommandHandler` checks the repository.
74/// Unlike `cleanup_data_environment`, this function works without a `TestContext` and is
75/// intended to be called early in the test setup before environment creation.
76///
77/// # Safety
78///
79/// This function is only intended for E2E test environments and should never
80/// be called in production code paths. It's designed to provide test isolation
81/// by ensuring environments from previous test runs don't interfere.
82///
83/// # Arguments
84///
85/// * `environment_name` - The name of the environment to clean up
86///
87/// # Returns
88///
89/// Returns `Ok(())` if cleanup succeeds or if the directory doesn't exist.
90///
91/// # Errors
92///
93/// Returns a `PreflightCleanupError::ResourceConflicts` error if the data directory
94/// cannot be removed due to permission issues or file locks.
95pub fn cleanup_previous_test_data(environment_name: &str) -> Result<(), PreflightCleanupError> {
96    use std::path::Path;
97
98    let data_dir = Path::new("data").join(environment_name);
99
100    if !data_dir.exists() {
101        info!(
102            operation = "preflight_data_cleanup",
103            status = "clean",
104            path = %data_dir.display(),
105            "No previous data directory found, skipping cleanup"
106        );
107        return Ok(());
108    }
109
110    info!(
111        operation = "preflight_data_cleanup",
112        path = %data_dir.display(),
113        "Cleaning data directory from previous test run"
114    );
115
116    match std::fs::remove_dir_all(&data_dir) {
117        Ok(()) => {
118            info!(
119                operation = "preflight_data_cleanup",
120                status = "success",
121                path = %data_dir.display(),
122                "Data directory cleaned successfully"
123            );
124            Ok(())
125        }
126        Err(e) => {
127            warn!(
128                operation = "preflight_data_cleanup",
129                status = "failed",
130                path = %data_dir.display(),
131                error = %e,
132                "Failed to clean data directory"
133            );
134            Err(PreflightCleanupError::ResourceConflicts {
135                details: format!(
136                    "Failed to clean data directory '{}': {}",
137                    data_dir.display(),
138                    e
139                ),
140            })
141        }
142    }
143}
144
145/// Generic directory cleanup function for E2E test preflight operations
146///
147/// This is the core cleanup function used by all directory cleanup operations.
148/// It removes the specified directory if it exists, with proper logging.
149///
150/// # Safety
151///
152/// This function is only intended for E2E test environments and should never
153/// be called in production code paths.
154///
155/// # Arguments
156///
157/// * `dir_path` - The path to the directory to clean
158/// * `operation_name` - A descriptive name for the operation (used in logs)
159/// * `description` - A human-readable description of what's being cleaned (used in logs)
160///
161/// # Returns
162///
163/// Returns `Ok(())` if cleanup succeeds or if the directory doesn't exist.
164///
165/// # Errors
166///
167/// Returns a `PreflightCleanupError::ResourceConflicts` error if the directory
168/// cannot be removed due to permission issues or file locks.
169pub fn cleanup_directory(
170    dir_path: &std::path::Path,
171    operation_name: &str,
172    description: &str,
173) -> Result<(), PreflightCleanupError> {
174    if !dir_path.exists() {
175        info!(
176            operation = operation_name,
177            status = "clean",
178            path = %dir_path.display(),
179            "{} doesn't exist, skipping cleanup", description
180        );
181        return Ok(());
182    }
183
184    info!(
185        operation = operation_name,
186        path = %dir_path.display(),
187        "Cleaning {} to ensure fresh state", description
188    );
189
190    match std::fs::remove_dir_all(dir_path) {
191        Ok(()) => {
192            info!(
193                operation = operation_name,
194                status = "success",
195                path = %dir_path.display(),
196                "{} cleaned successfully", description
197            );
198            Ok(())
199        }
200        Err(e) => {
201            warn!(
202                operation = operation_name,
203                status = "failed",
204                path = %dir_path.display(),
205                error = %e,
206                "Failed to clean {}", description
207            );
208            Err(PreflightCleanupError::ResourceConflicts {
209                details: format!(
210                    "Failed to clean {} '{}': {}",
211                    description,
212                    dir_path.display(),
213                    e
214                ),
215            })
216        }
217    }
218}
219
220/// Specification for a directory cleanup operation
221///
222/// This DTO holds all the information needed to clean up a single directory
223/// during preflight cleanup operations.
224#[derive(Debug, Clone)]
225pub struct DirectoryCleanupSpec {
226    /// The path to the directory to clean
227    pub path: std::path::PathBuf,
228    /// A descriptive name for the operation (used in logs)
229    pub operation_name: String,
230    /// A human-readable description of what's being cleaned (used in logs)
231    pub description: String,
232}
233
234impl DirectoryCleanupSpec {
235    /// Creates a new directory cleanup specification
236    ///
237    /// # Arguments
238    ///
239    /// * `path` - The path to the directory to clean
240    /// * `operation_name` - A descriptive name for the operation (used in logs)
241    /// * `description` - A human-readable description of what's being cleaned (used in logs)
242    #[must_use]
243    pub fn new(
244        path: impl Into<std::path::PathBuf>,
245        operation_name: impl Into<String>,
246        description: impl Into<String>,
247    ) -> Self {
248        Self {
249            path: path.into(),
250            operation_name: operation_name.into(),
251            description: description.into(),
252        }
253    }
254}
255
256/// Cleans multiple directories in sequence
257///
258/// This function iterates over a list of directory cleanup specifications
259/// and cleans each directory. If any cleanup fails, the function returns
260/// immediately with the error.
261///
262/// # Safety
263///
264/// This function is only intended for E2E test environments and should never
265/// be called in production code paths.
266///
267/// # Arguments
268///
269/// * `specs` - A slice of directory cleanup specifications
270///
271/// # Returns
272///
273/// Returns `Ok(())` if all cleanups succeed.
274///
275/// # Errors
276///
277/// Returns the first `PreflightCleanupError` encountered during cleanup.
278pub fn cleanup_directories(specs: &[DirectoryCleanupSpec]) -> Result<(), PreflightCleanupError> {
279    for spec in specs {
280        cleanup_directory(&spec.path, &spec.operation_name, &spec.description)?;
281    }
282    Ok(())
283}
284
285/// Cleans the build directory to ensure fresh template state for E2E tests
286///
287/// This function removes the build directory if it exists, ensuring that
288/// E2E tests start with a clean state and don't use stale cached template files.
289///
290/// # Safety
291///
292/// This function is only intended for E2E test environments and should never
293/// be called in production code paths. It's designed to provide test isolation
294/// by ensuring fresh template rendering for each test run.
295///
296/// # Arguments
297///
298/// * `env` - The test environment containing configuration paths
299///
300/// # Returns
301///
302/// Returns `Ok(())` if cleanup succeeds or if the build directory doesn't exist.
303///
304/// # Errors
305///
306/// Returns a `PreflightCleanupError::ResourceConflicts` error if the build directory
307/// cannot be removed due to permission issues or file locks.
308pub fn cleanup_build_directory(test_context: &TestContext) -> Result<(), PreflightCleanupError> {
309    cleanup_directory(
310        &test_context.config.build_dir,
311        "build_directory_cleanup",
312        "build directory",
313    )
314}
315
316/// Cleans the templates directory to ensure fresh embedded template extraction for E2E tests
317///
318/// This function removes the templates directory if it exists, ensuring that
319/// E2E tests start with fresh embedded templates and don't use stale cached template files.
320/// This is critical for testing template changes and instance name parameterization.
321///
322/// # Safety
323///
324/// This function is only intended for E2E test environments and should never
325/// be called in production code paths. It's designed to provide test isolation
326/// by ensuring fresh template extraction for each test run.
327///
328/// # Arguments
329///
330/// * `env` - The test environment containing configuration paths
331///
332/// # Returns
333///
334/// Returns `Ok(())` if cleanup succeeds or if the templates directory doesn't exist.
335///
336/// # Errors
337///
338/// Returns a `PreflightCleanupError::ResourceConflicts` error if the templates directory
339/// cannot be removed due to permission issues or file locks.
340pub fn cleanup_templates_directory(
341    test_context: &TestContext,
342) -> Result<(), PreflightCleanupError> {
343    let templates_dir = std::path::Path::new(&test_context.config.templates_dir);
344    cleanup_directory(
345        templates_dir,
346        "templates_directory_cleanup",
347        "templates directory",
348    )
349}
350
351/// Cleans the data directory for the test environment to ensure fresh state for E2E tests
352///
353/// This function removes the environment's data directory if it exists, ensuring that
354/// E2E tests start with a clean state and don't encounter conflicts with stale
355/// environment data from previous test runs. This prevents "environment already exists"
356/// errors and ensures proper test isolation.
357///
358/// # Safety
359///
360/// This function is only intended for E2E test environments and should never
361/// be called in production code paths. It's designed to provide test isolation
362/// by ensuring fresh environment state for each test run.
363///
364/// # Arguments
365///
366/// * `test_context` - The test context containing the environment configuration
367///
368/// # Returns
369///
370/// Returns `Ok(())` if cleanup succeeds or if the data directory doesn't exist.
371///
372/// # Errors
373///
374/// Returns a `PreflightCleanupError::ResourceConflicts` error if the data directory
375/// cannot be removed due to permission issues or file locks.
376pub fn cleanup_data_environment(test_context: &TestContext) -> Result<(), PreflightCleanupError> {
377    use std::path::Path;
378
379    // Construct the data directory path: data/{environment_name}
380    let data_dir = Path::new("data").join(test_context.environment.name().as_str());
381    cleanup_directory(&data_dir, "data_directory_cleanup", "data directory")
382}