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}